声明:

本系列博客是《算法竞赛进阶指南》+《算法竞赛入门经典》+《挑战程序设计竞赛》的学习笔记,主要是因为我三本都买了 按照《算法竞赛进阶指南》的目录顺序学习,包含书中的部分重要知识点、例题答案及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和复习,无任何商业用途。博客中部分内容来源于书本和网络(我尽量减少书中引用),由我个人整理总结(习题和代码可全都是我自己敲哒)部分内容由我个人编写而成 ,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》— 作者李煜东,强烈安利,好书不火系列,谢谢配合。

下方链接为学习笔记目录链接(中转站)

学习笔记目录链接

ACM-ICPC模板


一、栈

s t a c k ( l a s t <mtext>   </mtext> i n f i r s t <mtext>   </mtext> o u t ) (stack)(last \ in first\ out) stack(last infirst out)后进先出
基础的栈相信大家都懂,stack可以直接使用STL,或者用一个数组和一个变量(记录栈顶位置)来实现栈结构。

使用时都要注意判空,不然就会RE!!

0.AcWing 41. 包含min函数的栈 (自己造栈)

剑指Offer的面试类型的题。

关于输出Min,直接维护一个单调栈,栈顶存的就是当前栈的最小值。
这样各种操作都是 O 1 O(1) O1

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. 火车进出栈问题

方法一:搜索(枚举/递归) Θ ( 2 N ) \Theta (2^N) Θ(2N)面对任何一个状态,我们只有两种选择:

把下一个数进栈;
栈顶的数出栈
方法二:递推 Θ ( N 2 ) \Theta (N^2) Θ(N2)
如果只要求方案数,不需要具体的方案,可以使用递推直接统计:
S N S_N SN
表示进栈顺序为 1 , 2 , , N 1,2,⋯,N 1,2,,N时可能的出栈顺序总数,现在考虑数字1在出栈顺序中的位置,如果1排在第k个出栈,那么整个进出栈的过程为:

  1. 整数1进栈;
  2. 2 ~ k - 1这k - 2个数以某种顺序进出栈;
  3. 整数1出栈,排在第k个;
  4. k + 1 ~ N 这N - k个数字按照某种顺序进出栈;

于是可以得到递推公式:

S N = k = 1 N S k 1 S N k S_N = \sum_{k = 1}^{N} S_{k - 1} * S_{N - k} SN=k=1NSk1SNk

方法三:动态规划 Θ ( N 2 ) \Theta(N^2) Θ(N2)

F [ i , j ] F[i, j] F[i,j]表示有i个数尚未进栈,目前有j个数在栈中,有 n i j n - i - j nij个数已经出栈时的方案总数,边界条件:开始: F [ 0 , 0 ] = 1 F[0,0]=1 F[0,0]=1结束: F [ N , 0 ] F[N,0] F[N,0]开始: F [ 0 , 0 ] = 1 F[0, 0] = 1 \quad F[0,0]=1 结束: F [ N , 0 ] F[N, 0] F[N,0]开始: F [ 0 , 0 ] = 1 F[0,0]=1 F[0,0]=1结束:F[N,0];
由于每一步只能执行两种操作:把一个数进栈和把一个数出栈,所以递推公式为:
F [ i , j ] = F [ i 1 , j + 1 ] + F [ i , j 1 ] F[i,j]=F[i−1,j+1]+F[i,j−1] F[i,j]=F[i1,j+1]+F[i,j1]

方法四:数学 Θ ( N ) \Theta (N) Θ(N)
该问题等价于求第N项 C a t a l a n Catalan Catalan数,即 C 2 N N / ( N + 1 ) C_{2N}^{N} / (N + 1) C2NN/(N+1),将在第三章介绍。
以上解析均来自《算法竞赛进阶指南》

代码

代码

二、各种表达式计算

中缀表达式:最常见的表达式,如 3 ( 1 2 ) 3∗(1−2) 3(12)

前缀表达式:又称波兰式,例如 3 1 <mtext>   </mtext> 2 ∗3−1\ 2 31 2

后缀表达式:又称逆波兰式,例如 1 <mtext>   </mtext> 2 3 1 \ 2 - 3 1 23

后缀表达式可以在 Θ ( N ) \Theta (N) Θ(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)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧,日更博主每天在线答疑 ! 当然,也非常欢迎您能在讨论区指出此文的不足处,作者会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧