## 单调队列优化DP问题总结 本节二刷用了五天的时间,真是相当于困在这里,这一节的思想很简单,但细节很多,不好理解(~~主要是因为笨~~),现把自己的领悟写一下,给后来者一些启示,水平有限,错误在所难免,勿喷! ### 一、$DP$优化的步骤 先不要考虑优化,先把$DP$模型搞清楚,**写出基础代码,$TLE$后再考虑如何优化**,用什么优先,这才是学习的正道,不能是上来就眉毛胡子一把抓,能学懂才是见了鬼了!本节六道题我全部采用了此策略,每篇题解中也是提供了基础的$DP$代码,然后再利用单调队列进行优化。不会走就想着跑,肯定不行,以后的学习中要注意这个策略。 ### 二、状态表示的确定 这个东西,$yxc$大佬说主要靠经验,想想也是,如果我没有做过**烽火传递**这道题,天知道为什么要设置$f[i]$为前$i-1$个合法,第$i$个为选中状态的表示? 而且这么表示后,还需要遍历一次尾巴上的$i \sim i-m$个才能找出答案,反正让我直接想我想不出来,学习了之后,再看后面的那道绿色通道,一下子就明白该怎么样进行状态定义了,这就是**经验**。经验靠什么来呢?当然是题量,**经典题的题量**,没有大量的经典题刷题,是不可能建立自己的知识图的,如果想学习,还想双减,那是不可能学会的。 ### 三、单调队列之哨兵 在单调队列中使用哨兵,主要是在第$1$个枚举到的数,它依赖的滑动窗口此时为空,无法获取到$head$,那么此时的处理逻辑是什么样呢? 一般来讲从两个方面来考虑: * 是不是窗口长度越界,需要出队首元素? ```c++ while (hh <= tt && q[hh] < i - m) hh++; ``` 这种情况下,由于是第$1$个枚举到的数,肯定不可能窗口长度越界,如果按上面的写法,`hh=0 tt=-1`,此时`hh>tt`,所以`while`不执行,直接短路运算,没机会讨论`q[hh]`是否小于`i-m`,这里是安全的。 但有的写法,这里是这样的 ```c++ if(q[hh] < i-m) hh++; ``` 此时,没有了`hh<=tt`的保护,这里队列中还没有元素,但是却访问`q[hh]`是否小于`i-m`,这就是在物理意义上有问题了!此处我的建议是不管有用没用,统一使用: ```c++ while (hh <= tt && q[hh] < i - m) hh++; ``` 这么写没毛病,别手懒! * 使用队列头 ```c++ res = max(res, s[i] - s[q[hh]]); ``` 此时如果没有哨兵,同样存在着无法直接访问使用`q[hh]`的问题,**造成代码逻辑上出错**。 在使用哨兵前,我们要思考如果要虚拟出一个哨兵,它要解决什么问题? (1)队列天生不是空的,随时可以取队列头 (2)第$1$个数字的运算逻辑和其它数字没有区别,不用特殊判断 **总结** (1)增加哨兵能简单的解决边界问题 (2)依赖于前序关系的需要加哨兵,不依赖前序关系的可以加哨兵,也可以不加哨兵 (3)如果想要加哨兵,需要遵照下面的原则 > 如果是逆时针,正序遍历,那么添加的哨兵应该是第$0$个,$q[++tt]=0$ > 如果是顺时针,倒序遍历,那么添加的哨兵应该是第$n+1$个,$q[++tt]=n+1$ [AcWing 154. 滑动窗口](https://www.acwing.com/problem/content/156/) ```c++ int hh = 0, tt = -1; q[++tt]=0; for (int i = 1; i <= n; i++) { while (hh <= tt && i + 1 - k > q[hh]) hh++; while (hh <= tt && a[q[tt]] >= a[i]) tt--; q[++tt] = i; if (i >= k) printf("%d ", a[q[hh]]); } ``` 此题,枚举到的$i$号元素,**不依赖于前序进行推导计算**,直接输出前序即可,**加不加哨兵都是一样**的。 [AcWing 135. 最大子序和](https://www.acwing.com/problem/content/137/) ```c++ int hh = 0, tt = -1; q[++tt] = 0; //放入哨兵结点 int res = -INF; //预求最大,先设最小 for (int i = 1; i <= n; i++) { while (hh <= tt && q[hh] < i - m) hh++; res = max(res, s[i] - s[q[hh]]); while (hh <= tt && s[q[tt]] >= s[i]) tt--; q[++tt] = i; } ``` 此题,枚举到的$i$号元素,**依赖前序进行推导计算**,**需要加哨兵**,并且**窗口中不包含$i$,在$i$加入窗口之前进行计算**。 [AcWing 1087. 修剪草坪](https://www.acwing.com/problem/content/description/1089/) ```c++ int hh = 0, tt = -1; q[++tt] = 0; for (int i = 1; i <= n; i++) { while (hh <= tt && i - q[hh] > m) hh++; f[i] = max(f[i - 1], f[max(0, q[hh] - 1)] + s[i] - s[q[hh]]); while (hh <= tt && f[i - 1] - s[i] >= f[max(0, q[tt] - 1)] - s[q[tt]]) tt--; q[++tt] = i; } ``` 此题,枚举到的$i$号元素,**依赖前序进行推导计算**,**需要加哨兵**,并且**窗口中不包含$i$,在$i$加入窗口之前进行计算**。