You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
python/TangDou/AcWing/DP/MonotonicQueue/单调队列优化DP问题总结.md

4.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

单调队列优化DP问题总结

本节二刷用了五天的时间,真是相当于困在这里,这一节的思想很简单,但细节很多,不好理解(主要是因为笨,现把自己的领悟写一下,给后来者一些启示,水平有限,错误在所难免,勿喷!

一、DP优化的步骤

先不要考虑优化,先把DP模型搞清楚,写出基础代码,TLE后再考虑如何优化,用什么优先,这才是学习的正道,不能是上来就眉毛胡子一把抓,能学懂才是见了鬼了!本节六道题我全部采用了此策略,每篇题解中也是提供了基础的DP代码,然后再利用单调队列进行优化。不会走就想着跑,肯定不行,以后的学习中要注意这个策略。

二、状态表示的确定

这个东西,yxc大佬说主要靠经验,想想也是,如果我没有做过烽火传递这道题,天知道为什么要设置f[i]为前i-1个合法,第i个为选中状态的表示? 而且这么表示后,还需要遍历一次尾巴上的i \sim i-m个才能找出答案,反正让我直接想我想不出来,学习了之后,再看后面的那道绿色通道,一下子就明白该怎么样进行状态定义了,这就是经验。经验靠什么来呢?当然是题量,经典题的题量,没有大量的经典题刷题,是不可能建立自己的知识图的,如果想学习,还想双减,那是不可能学会的。

三、单调队列之哨兵

在单调队列中使用哨兵,主要是在第1个枚举到的数,它依赖的滑动窗口此时为空,无法获取到head,那么此时的处理逻辑是什么样呢?

一般来讲从两个方面来考虑:

  • 是不是窗口长度越界,需要出队首元素?
while (hh <= tt && q[hh] < i - m) hh++; 

这种情况下,由于是第1个枚举到的数,肯定不可能窗口长度越界,如果按上面的写法,hh=0 tt=-1,此时hh>tt,所以while不执行,直接短路运算,没机会讨论q[hh]是否小于i-m,这里是安全的。

但有的写法,这里是这样的

if(q[hh] < i-m) hh++;  

此时,没有了hh<=tt的保护,这里队列中还没有元素,但是却访问q[hh]是否小于i-m,这就是在物理意义上有问题了!此处我的建议是不管有用没用,统一使用:

while (hh <= tt && q[hh] < i - m) hh++; 

这么写没毛病,别手懒!

  • 使用队列头
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. 滑动窗口

  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. 最大子序和

   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. 修剪草坪

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加入窗口之前进行计算