单调队列不是传统意义上的FIFO的结构,主要用于维护一段固定长度区间的属性。
基础模版
滑动窗口:求对于已知序列每个元素的一段固定长度为k的区间 中的最大值。单调队列存放遍历到的元素对应的窗口中的 单调递减顺序排列 的元素 的下标,单调队列的首元素即为所求值的下标。每个元素最多进队和出队各一次,所以时间复杂度O(n)。
int hh=0,tt=-1;
//如果下面遍历从1开始,这两个初始值也不需要+1,因为它是相对位置。
for(int i=0;i<n;i++){
if(hh<=tt && q[hh]<i-k+1) hh++;
//首先窗口移动一格,队首元素弹出,注意是i-k+1
while(hh<=tt && q[tt]<=a[i]) --tt;
//如果遍历到的元素比队尾元素大就弹出队尾,给它腾出位使得窗口序列严格单调,注意取等
q[++tt]=a[i];
//最后,腾出来就可以入队了
if(i>k-1) cout<<a[q[hh]]<<” ”;
//最开始的时候坐标小于队列长度,说明队列没满,即区间长度不满足k,不输出
}
单调队列优化DP
优化都是在朴素dp的基础上进行的,所以一定要先清楚朴素dp的思路
1.最大子序和:
求一个数组的长度不超m的子区间(连续)和最大值。
暴力思路是前缀和优化枚举区间,dp思路是枚举区间右端点。
状态表示:dp[i]是以a[i]为右端点的长度不超过m的连续子区间和
状态属性:区间和最大值
状态计算:f[i] = max{sum[i]-sum[j]} (1<= i - j <=m)
= sum[i]-min{sum[j]} (i-m<= j <=i-1)
这里有个数学常识,有些人没注意到就会影响式子理解:所求的f值是与i相关的,是关于i的状态的计算,这时只和i相关的值算作常量,可以提出来。
观察变形式子发现,从前向后维护一个长度不超m的区间最小值,用单调队列维护区间左端点,最终答案是所有f[i]的最大值。
int hh=0,tt=0;
for(int i=1;i<=n;i++){
if(hh<=tt && i-q[hh]>m) hh++;
//这里注意范围,理由见下方文字
res=max(res,s[i]-s[q[hh]]);
while(hh<=tt && s[q[tt]]>=s[i]) --tt;
q[++tt]=i;
}
注意tt=-1是适用于原数组元素的模版,而tt=0是适用于前缀和的模版,相当于push s[0]下标。因为以a[i]结尾的子串左端点范围是[i-m+1, i],求区间和公式为s[r]-s[l-1],则窗口维护的l∈[i-m, i-1],因此q[hh]最小取i-m,并且窗口内取不到i。
修剪草坪
求正整数数组一个子序列(非连续)使得序列和最大。其中序列中的元素在原数组连续相邻超过m个则他们的和变为0→子序列长度不超过m。
y总方法看题解区第二篇,这里用逆向求解更好理解:
题意转化为原数组中每连续k+1个数里就必须去除一个不选,使去除的元素之和(损失值)最小,从而使答案即 总和-损失值 最大。窗口维护元素是去除的元素(单增),队首为最小元素。(剩下的过程详见5)
旅行问题(待补)
一个环n个点,第i个点可以给车加p[i]的油,从i到i+1要耗d[i]的油。车从任意一个点出发,过程中油必须是非负,判断能否顺时针或逆时针回到起点。
环形dp转线性,从任意两点间(不妨从n~1号点)将环断开,将此链复制一份接到原链后面。则从任意点出发回到起点的路径,对应链上从该点开始的长度为 n+1 的区间,这个区间用滑动窗口维护,
由题意得到达i点的条件是此前剩余油+该点获得油>=到该点的消耗油
理想的正方形(待补)
求a*b矩阵中边长为n的子正方形中的所有元素的 最大值-最小值 的最小值。
烽火传递
每连续m个数就必须选至少1个,求总贡献最小值。
状态表示:f[i]是选择a[i]且[1~i-1]合法的方案
状态属性:1~i的最小总贡献
状态计算:f[i] = min{w[i]+f[j]} (0<=i-j-1<=m-1)
= w[i]+min{f[j]} (1<=i-j<=m)
注意我们可以确定答案的范围一定在[n-m+1~n],所以y总输出答案的方法是遍历f[n-m+1]到f[n]取最小。其实可以利用滑动窗口的性质输出答案:第n次滑动时j的范围是[n-m~n-1],要想让j的范围为答案的范围,就再滑一位,由于队首元素为该区间的最小值,所以它就是答案。