101 lines
4.9 KiB
Markdown
101 lines
4.9 KiB
Markdown
|
|
## 单调队列优化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$加入窗口之前进行计算**。
|