0x11.基本數據結構—棧


聲明:

本系列博客是《算法競賽進階指南》+《算法競賽入門經典》+《挑戰程序設計競賽》的學習筆記,主要是因爲我三本都買了 按照《算法競賽進階指南》的目錄順序學習,包含書中的部分重要知識點、例題答案及我個人的學習心得和對該算法的補充拓展,僅用於學習交流和複習,無任何商業用途。博客中部分內容來源於書本和網絡(我儘量減少書中引用),由我個人整理總結(習題和代碼可全都是我自己敲噠)部分內容由我個人編寫而成 ,如果想要有更好的學習體驗或者希望學習到更全面的知識,請於京東搜索購買正版圖書:《算法競賽進階指南》— 作者李煜東,強烈安利,好書不火系列,謝謝配合。

下方鏈接爲學習筆記目錄鏈接(中轉站)

學習筆記目錄鏈接

ACM-ICPC模板


一、棧

stack(last infirst out)(stack)(last \ in first\ out)後進先出
基礎的棧相信大家都懂,stack可以直接使用STL,或者用一個數組和一個變量(記錄棧頂位置)來實現棧結構。

使用時都要注意判空,不然就會RE!!

0.AcWing 41. 包含min函數的棧 (自己造棧)

在這裏插入圖片描述

劍指Offer的面試類型的題。

關於輸出Min,直接維護一個單調棧,棧頂存的就是當前棧的最小值。
這樣各種操作都是O1O(1)

class MinStack {
public:
    /** initialize your data structure here. */
    stack<int>stackValue;
    stack<int>stackMin;
    MinStack() {
        
    }
    
    void push(int x) {
        stackValue.push(x);
        if(stackMin.empty()||stackMin.top()>=x){
            stackMin.push(x);
        }
    }
    
    void pop() {
        if(stackValue.top()==stackMin.top())
            stackMin.pop();
        stackValue.pop();
    }
    
    int top() {
        return stackValue.top();
    }
    
    int getMin() {
        return stackMin.top();
    }
};

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

1.AcWing 128. 編輯器 (對頂棧)

在這裏插入圖片描述

memset(f,0xcf,sizeof f);

-8084644332

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<map>
#include<vector>
#include<queue>
#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;

typedef long long ll;//全用ll可能會MLE或者直接WA,全部換成int看會不會A,別動這裏!!!
const int N=2000007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方約等於趨近爲0

int n,m;
int stack_l[N],stack_r[N];//一對對頂棧
int sum[N];//前綴和
int f[N];//最大前綴和
int cntL,cntR;//棧頂元素

int main()
{
    scanf("%d",&n);
    memset(f,0xcf,sizeof f);
    over(i,1,n){
        char ch;
        cin>>ch;
        //ch=getchar();
        if(ch=='I'){//插入元素至L
            int x;
            scanf(" %d",&x);
            stack_l[++cntL]=x;
            sum[cntL]=sum[cntL-1]+x;
            f[cntL]=max(f[cntL-1],sum[cntL]);
        }
        else if(ch=='Q'){//詢問
            int x;
            scanf(" %d",&x);
            printf("%d\n",f[x]);
        }
        else if(ch=='D'){//刪除元素
            if(!cntL)continue;//判空,要是用STL也必須要判空
            cntL--;
        }
        else if(ch=='L'){
            if(!cntL)continue;
            stack_r[++cntR]=stack_l[cntL--];
        }
        else if(ch=='R'){
            if(!cntR)continue;
            stack_l[++cntL]=stack_r[cntR--];
            sum[cntL]=sum[cntL-1]+stack_l[cntL];
            f[cntL]=max(sum[cntL],f[cntL-1]);
        }
        //getchar();
    }
    return 0;
}

也可以使用STL,我懶得敲了,直接放一個大佬的吧:

作者:秦淮岸燈火闌珊
鏈接:https://www.acwing.com/solution/AcWing/content/1275/

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int t,x,sum[N],f[N],now;
stack<int> a,b,c;
int main()
{
    while(scanf("%d\n",&t)!=EOF)//之前在HDU提交,所以是多組數據
    {
        a=c;//STL特性,這裏就是清空操作
        b=c;
        f[0]=-1e7;//初始化
        sum[0]=0;
        for(int i=1;i<=t;i++)
        {
            char ch=getchar();//讀入
            if (ch=='I')//插入操作
            {
                scanf(" %d",&x);
                a.push(x);//將a插入棧中
                sum[a.size()]=sum[a.size()-1]+a.top();//前1~a.size()-1的前綴和,加上這個一個新來的,構成1~a.size()
                f[a.size()]=max(f[a.size()-1],sum[a.size()]);//看是之前的最大值大,還是新來的最大值大
            }
            if (ch=='D')
                if (!a.empty())//只要棧不爲空,就刪除
                    a.pop();
            if (ch=='L')//左傾思想(博古+文化大革命)(手動滑稽)
                if(!a.empty())//只要不爲空
                    b.push(a.top()),a.pop();//a+b等於整個插入序列,b負責管理當前光標右邊的序列.
            if (ch=='R')//右傾思想(陳獨秀)(手動滑稽)
            {
                if (!b.empty())//b不爲空
                {
                    a.push(b.top());//a負責管理1~當前光標.所以現在a往右了,那麼必然是要加入b棧的開頭,因爲b棧管理當前光標的右邊.
                    b.pop();
                    sum[a.size()]=sum[a.size()-1]+a.top();//同樣的還是重新定義.
                    f[a.size()]=max(f[a.size()-1],sum[a.size()]);//見插入操作.
                }
            }
            if (ch=='Q')
            {
                scanf(" %d",&x);
                printf("%d\n",f[x]);//輸出當前最大值區間.
            }
            getchar();//換行符讀入
        }
    }
    return 0;
}


2.AcWing 129. 火車進棧

在這裏插入圖片描述

因爲對於每一步我們只有兩種操作,入棧或者棧頂出棧。

選擇出棧得到的序列一定比選擇入棧最後得到的序列的字典序要小,所以DFS爆搜,先搜pop再搜push,這樣就會得到按照字典序排列的答案瞭然後維護好邊界就好。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<stack>
#include<vector>

#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;

typedef long long ll;//全用ll可能會MLE或者直接WA,全部換成int看會不會A,別動這裏!!!
const int N=1000007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方約等於趨近爲0

int n;
vector<int>ans;
stack<int>st;


//我們只有兩種操作,入棧或者棧頂出棧
//選擇出棧得到的序列一定比選擇入棧最後得到的序列的字典序要小
int cnt=20;
void dfs(int u)
{
    if(!cnt)return ;
    if(ans.size()==n){
        cnt--;
        for(auto x:ans)
            cout<<x;
        cout<<endl;
        return ;
    }
    if(st.size()){
        ans.push_back(st.top());
        st.pop();
        dfs(u);
        st.push(ans.back());//回溯
        ans.pop_back();
    }
    if(u<=n){
        st.push(u);
        dfs(u+1);
        st.pop();
    }
}


int main()
{
    scanf("%d",&n);
    dfs(1);
    return 0;
}

3.AcWing 130. 火車進出棧問題

在這裏插入圖片描述

方法一:搜索(枚舉/遞歸)Θ(2N)\Theta (2^N)面對任何一個狀態,我們只有兩種選擇:

把下一個數進棧;
棧頂的數出棧
方法二:遞推Θ(N2)\Theta (N^2)
如果只要求方案數,不需要具體的方案,可以使用遞推直接統計:
SNS_N
表示進棧順序爲1,2,,N1,2,⋯,N時可能的出棧順序總數,現在考慮數字1在出棧順序中的位置,如果1排在第k個出棧,那麼整個進出棧的過程爲:

  1. 整數1進棧;
  2. 2 ~ k - 1這k - 2個數以某種順序進出棧;
  3. 整數1出棧,排在第k個;
  4. k + 1 ~ N 這N - k個數字按照某種順序進出棧;

於是可以得到遞推公式:

SN=k=1NSk1SNkS_N = \sum_{k = 1}^{N} S_{k - 1} * S_{N - k}

方法三:動態規劃Θ(N2)\Theta(N^2)

F[i,j]F[i, j]表示有i個數尚未進棧,目前有j個數在棧中,有nijn - i - j個數已經出棧時的方案總數,邊界條件:開始:F[0,0]=1F[0,0]=1結束:F[N,0]F[N,0]開始:F[0,0]=1F[0, 0] = 1 \quad 結束:F[N,0]F[N, 0]開始:F[0,0]=1F[0,0]=1結束:F[N,0];
由於每一步只能執行兩種操作:把一個數進棧和把一個數出棧,所以遞推公式爲:
F[i,j]=F[i1,j+1]+F[i,j1]F[i,j]=F[i−1,j+1]+F[i,j−1]

方法四:數學Θ(N)\Theta (N)
該問題等價於求第N項CatalanCatalan數,即C2NN/(N+1)C_{2N}^{N} / (N + 1),將在第三章介紹。
以上解析均來自《算法競賽進階指南》

代碼

代碼

二、各種表達式計算

中綴表達式:最常見的表達式,如3(12)3∗(1−2)

前綴表達式:又稱波蘭式,例如31 2∗3−1\ 2

後綴表達式:又稱逆波蘭式,例如1 231 \ 2 - 3

後綴表達式可以在Θ(N)\Theta (N)的時間內求值。

後綴表達式求值方式:
建立一個棧,從左往右掃描表達式:

  1. 遇到數字,入棧
  2. 遇到運算符,彈出棧中的兩個元素,計算結果後再將結果壓入棧掃描完成之後,棧中只剩下一個數字,最終結果。

中綴表達式轉後綴表達式:

  • 建立一個用於儲存運算符的棧,逐一掃描中綴表達式中的元素
  1. 掃描到數字,輸出該數;
  2. 遇到左括號,將左括號入棧;
  3. 遇到右括號,不斷取出棧頂元素並且輸出,直到棧頂爲左括號,彈出左括號舍棄
  4. 遇到運算符,如果棧頂的運算符優先級大於當前掃描到的運算符,就不斷取出棧頂的元素,最後將新符號入棧;
  • 將棧中剩餘的運算符輸出,所有的輸出結果即爲轉化後的後綴表達式。

遞歸法求中綴表達式的值,O(n^2)

int calc(int l, int r) {
	// 尋找未被任何括號包含的最後一個加減號
	for (int i = r, j = 0; i >= l; i--) {
		if (s[i] == '(') j++;
		if (s[i] == ')') j--;
		if (j == 0 && s[i] == '+') return calc(l, i - 1) + calc(i + 1, r);
		if (j == 0 && s[i] == '-') return calc(l, i - 1) - calc(i + 1, r);
	}
	// 尋找未被任何括號包含的最後一個乘除號
	for (int i = r, j = 0; i >= l; i--) {
		if (s[i] == '(') j++;
		if (s[i] == ')') j--;
		if (j == 0 && s[i] == '*') return calc(l, i - 1) * calc(i + 1, r);
		if (j == 0 && s[i] == '/') return calc(l, i - 1) / calc(i + 1, r);
	}
	// 首尾是括號
	if (s[l] == '('&&s[r] == ')') return calc(l + 1, r - 1);
	// 是一個數
	int ans = 0;
	for (int i = l; i <= r; i++) ans = ans * 10 + s[i] - '0';
	return ans;
}

後綴表達式轉中綴表達式,同時求值,O(n)


// 數值棧 
vector<int> nums; 
// 運算符棧 
vector<char> ops;

// 優先級 
int grade(char op) {
	switch (op) {
	case '(':
		return 1;
	case '+':
	case '-':
		return 2;
	case '*':
	case '/':
		return 3;
	}
	return 0;
}

// 處理後綴表達式中的一個運算符 
void calc(char op) {
	// 從棧頂取出兩個數 
	int y = *nums.rbegin();
	nums.pop_back();
	int x = *nums.rbegin();
	nums.pop_back();
	int z;
	switch (op) {
	case '+':
		z = x + y;
		break;
	case '-':
		z = x - y;
		break;
	case '*':
		z = x * y;
		break;
	case '/':
		z = x / y;
		break;
	}
	// 把運算結果放回棧中 
	nums.push_back(z);	
}

中綴表達式轉後綴表達式,同時對後綴表達式求值

int solve(string s) {
	nums.clear();
	ops.clear();
	int top = 0, val = 0;
	for (int i = 0; i < s.size(); i++) {
		// 中綴表達式的一個數字 
		if (s[i] >= '0' && s[i] <= '9') {
			val = val * 10 + s[i] - '0';
			if (s[i+1] >= '0' && s[i+1] <= '9') continue;
			// 後綴表達式的一個數,直接入棧 
			nums.push_back(val);
			val = 0;
		}
		// 中綴表達式的左括號 
		else if (s[i] == '(') ops.push_back(s[i]);
		// 中綴表達式的右括號 
		else if (s[i] == ')') {
			while (*ops.rbegin() != '(') {
				// 處理後綴表達式的一個運算符 
				calc(*ops.rbegin());
				ops.pop_back();
			}
			ops.pop_back();
		}
		// 中綴表達式的加減乘除號 
		else {
			while (ops.size() && grade(*ops.rbegin()) >= grade(s[i])) {
				calc(*ops.rbegin());
				ops.pop_back();
			}
			ops.push_back(s[i]);
		} 
	}
	while (ops.size()) {
		calc(*ops.rbegin());
		ops.pop_back();
	}
	// 後綴表達式棧中最後剩下的數就是答案 
	return *nums.begin();
}


三、單調棧

單調棧這個東西還是很容易理解的,就是一個棧,維護好他的單調性,可以是單調遞增也可以是單調遞減(或者非嚴格單增等等 )。寫起來非常好寫, 就是如果當前要入棧的元素大於棧頂就push進去,如果小於就一直pop,直到當前元素大於棧頂元素或者棧空爲止,很容易就可以證明/看出來這個棧依照這樣的操作一定能保持單調。那麼這樣的單調棧到底有什麼作用呢 ?比如下面這道題。
在這裏插入圖片描述
輸入樣例:

7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0

輸出樣例:

8
4000

這道題要求最大面積,看上去沒什麼思路,其實就是單調棧的最基本的應用。
我畫幾個圖就能非常直觀地感受這道題了,不過在此之前最好先看一下代碼,然後再看圖。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<math.h>
#include<stack>
#include<vector>

#define ls (p<<1)
#define rs (p<<1|1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
typedef pair<double,double> PDD;

typedef long long ll;//全用ll可能會MLE或者直接WA,全部換成int看會不會A,別動這裏!!!
const int N=100007;
const ll mod=1e9+7;
const double EPS=1e-5;//-10次方約等於趨近爲0

ll q[N],w[N],h;
int n;


int main()
{
    while(scanf("%d",&n)&&n){
        memset(q,-1,sizeof q);
        int top=0;
        ll ans=0;
        over(i,1,n+1){
            if(i!=n+1){
                scanf("%lld",&h);
            }
            else h=0;
            if(h>q[top])//高於棧頂元素,保持遞增就入棧
                q[++top]=h,w[top]=1;
            else {
                ll cnt=0;
                while(h<=q[top]){//單調被破壞就pop,把所有低於這個元素的全部pop並更新答案。
                    ans=max(ans,(cnt+w[top])*q[top]);
                    cnt+=w[top--];
                }
                q[++top]=h;
                w[top]=cnt+1;
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

看完代碼是不是有一點懂了,那麼我們來看圖:

在這裏插入圖片描述
首先圖一是一組數據,畫成圖的樣子。我們維護單調棧的單調性,直到遇見違反單調性的數,我們pop棧頂元素並更新ans。棧頂大於要入棧的元素,就pop,ans=max(ans,s1)。第二個還是大於,同樣的操作,ans=max(ans,s2)。注意這裏的s2就是第二個高度乘以2,因爲第一個棧頂雖然pop了但是對於第二個棧頂來說增加了它的面積,所以用cnt加上這裏的寬度,更新答案。最後恢復單調性,最新的棧頂元素的寬度就是被刪除的元素的寬度與自己的總和。刪完之後雖然棧裏的元素少了幾個但是畫成圖確實沒有少元素,只是高度變了。如下圖圖二:
在這裏插入圖片描述
然後我們繼續上面的操作。最後求得最大值,如圖3。注意我們要在最後加一個高度爲0的數據,爲了避免如圖4的情況出現。
在這裏插入圖片描述
然後就是《算法競賽進階指南》這本書上的代碼:

a[n + 1] = p = 0;
for (int i = 1; i <= n + 1; i++) {
	if (a[i] > s[p]) {
		s[++p] = a[i], w[p] = 1;
	} else {
		int width=0;
		while (s[p] > a[i]) {
			width += w[p];
			ans = max(ans, (long long)width * s[p]);
			p--;
		}
		s[++p] = a[i], w[p] = width + 1;
	}
}

相信看完圖您一定能非常直觀地理解其中的奧妙。

注:如果您通過本文,有(qi)用(guai)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意爲您解答!( •̀ ω •́ )✧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章