线性的基本数据结构
栈
先进后出,没啥好讲的,主要是应用。
关于表达式
栈可以做表达式相关的问题。
几个名词:
逆波兰式是后缀表达式,波兰式是前缀表达式。
后缀表达式求值
可以用栈做到\(O(n)\)。
- 建立一个栈,储存表达式中的数字。
2.从前往后扫后缀表达式。遇到数字就入栈,遇到运算符就从栈中取出数字做运算,运算结果再入栈。注意减这种有顺序的运算。
- 最后栈中还剩一个数,即答案。
中缀表达式求值
Sol_1:
先把中缀表达式转成后缀表达式,再求值,\(O(n)\)。
-
建立一个栈,储存表达式中的运算符。
-
从前往后扫表达式,开始大力分讨:若是数字,输出;若是
(
,入栈;若是)
,不断输出栈顶直到栈顶为(
,再将(
出栈但不用输出;若是运算符\(op\),不断取出栈顶直到\(op\)的优先级大于栈顶,再将\(op\)入栈(优先级:*/
>+-
>(
)。 -
最后依次取出栈中剩余元素并输出。
转换之后再对后缀表达式求值即可。
Sol_2:
可以递归,\(O(n^2)\)。
考虑求出中缀表达式中\([l,r]\)的值,我们要求的就是\([1,n]\)。
-
考虑没有被任何括号包含的运算符,先考虑加减,再考虑乘除:若存在加减号,选择最靠右的一个,分左右两边递归;若存在乘除号,选最靠右的一个,分左右两边递归。
-
若不存在没有被任何括号包含的运算符:若首尾都是括号,则返回\([l+1,r-1]\)的答案;否则这个区间是一个数字,返回其数值。
表达式树
更强大的处理表达式的东西,通过栈建出来。
-
建立一个栈,储存表达式树的节点编号。
-
从前往后扫后缀表达式:若是数字,新建一个节点,以当前数字为值,左右儿子为空,入栈;若是运算符,新建一个节点,以当前运算符为值,先从栈中取\(r\),再从栈中取\(l\),合并起来再入栈,注意左右儿子取出的顺序!
表达式树一定是一棵二叉树。其前序遍历为前缀表达式,中序遍历为中缀表达式,后序遍历为后缀表达式。
建好后就可以DFS查询了,注意根据题意剪枝。
对顶栈
典:维护一段文本以及光标,支持光标处插入删除,光标处查询,前后移动光标。
Sol:
光标前后维护两个栈即可。
单调栈
就是保证栈内的元素具有单调性,是类似单调队列的东西。
二维的查询就尝试压缩到一维上,将信息拍平。
栈的神秘操作
\(Question\):
维护一个栈,支持插入删除,查最大次大。
Sol:
入栈出栈可以看成建树的过程。入栈就是跳到儿子,出栈就是跳到父亲。查最大次大就是查树上前缀最大次大。于是可以维护了。
也可以把最大值单独维护一下,其他的元素塞到set里面。
栈的终极Trick:baka's trick
首先思考双栈模拟双端队列。
Sol:
维护两个栈,使得两个栈拼起来就是要维护的队列。
当一个栈为空却要pop
时,将另一个栈从中间砍成两半,暴力重构两个栈的信息。
可以证明是\(O(n)\)的。
现在来思考这个东西与双指针的关系。
我们发现维护双指针很像维护双端队列。
baka's trick可以解决双指针中这样的困境:
结果便于支持加入,合并,但删除的复杂度错了。
可以联系一下回滚莫队,我们把删除操作改为撤销操作,不能撤销了就暴力重构。
具体而言:
-
在两个指针\(l,r\)之间再维护一个\(mid\),初始时\(mid=r\)。
-
我们不再单纯地维护\([l,r]\)这一段的值,而是维护\([l,mid]\)这一段的后缀信息,以及\((mid,r]\)这一段的前缀信息。
-
若移动指针后\(l>mid\),便使\(mid\gets r\),然后暴力重构\([l,r]\)之间的信息。
-
查询时将两段拼起来就好。
这样子在均摊下是对的,但我不会证。
队列
先进先出,没啥好讲的,主要是运用。
单调队列
很强,可以优化DP,或者自成一题。优化DP的方式看DP去。
单调队列保证队列中的东西具有单调性,方便查询最值(或类似的东西)。
一种常见模型是单调队列配合双指针。观察题目性质可以发现指针移动的单调性,然后两个指针之间用单调队列维护一下。
例如:
Luogu P3594
给定一个长度为 \(n\) 的序列,你有一次机会选中一段连续的长度不超过 \(d\) 的区间,将里面所有数字全部修改为 \(0\)。请找到最长的一段连续区间,使得该区间内所有数字之和不超过 \(p\)。
对于 \(100\%\) 的数据,\(1 \le d \le n \le 2 \times 10^6\),\(0 \le p \le 10^{16}\),\(1 \leq w_i \leq 10^9\)。
Sol:
显然无论如何改成\(0\)的区间的长度要尽量取到\(d\),设前缀和为\(sum_i\),该区间的右端点为\(i\),则删掉的数的和为\(sum_i-sum_{i-d}\)。
观察一下,对于一段合法的区间\([l,r]\),\(r\)增大时,\(l\)单调不减。
所以双指针套单调队列。
实现细节见代码。
Code
#include<bits/stdc++.h>using namespace std;
typedef long long ll;const int maxn=2e6+10;
ll n,p,d,h,t,ans,w[maxn],sum[maxn],q[maxn];ll max(ll a,ll b){return a>b?a:b;
}int main(){scanf("%lld%lld%lld",&n,&p,&d);for(int i=1;i<=n;++i){scanf("%lld",&w[i]);sum[i]=sum[i-1]+w[i];}int j=1;h=1,t=1;q[1]=d;ans=d;for(int i=d+1;i<=n;++i){while(h<=t&&sum[i]-sum[i-d]>sum[q[t]]-sum[q[t]-d]) t--;q[++t]=i;while(h<=t&&sum[i]-sum[j-1]-(sum[q[h]]-sum[q[h]-d])>p){j++;while(h<=t&&q[h]-d+1<j) h++;}if(sum[i]-sum[j-1]-(sum[q[h]]-sum[q[h]-d])<=p) ans=max(ans,i-j+1);}printf("%lld\n",ans);return 0;
}