有三种(DP)顺序
(1.)先枚举区间长度,再枚举左端点,再枚举断点,因为区间长度从小到大,所以子状态一定都被更新,切忌先枚举左端点。
(2.)倒序枚举,即左端点((i))(n->1),右端点((j))(i->n),断点(i-->j)。
(3.)记忆化搜索,这样做无需考虑枚举顺序
(4.)倍增思想,(dp[i-1][dp[i-1][j]]),见上面链接
需要注意几点:
(1.)注意初始化区间长度为(1)时的(DP)值,求最小值还需要初始化所有区间为最大值
(2.)对于环应该先将它断开变成两倍长的链再区间(DP),最后对于所有(dp[i][i+n])取答案
(3.)有时候最优答案不一定会出现在(dp[1][n]),需要对于(DP)过程中每一个值取最优值。
(DP)优化
一、四边形不等式
使用范围:区间序列(DP)求最小值(一定是最小值)
对于动态规划转移方程
dp[i][j]=min(dp[i][k],dp[k+1][j])+w(i,j);
其中(w(i,j))只受(i,j)取值影响
如果满足下面两个条件
(1.)区间单调性:如果对于(forall i leq i'< j' leq j,w(i',j') leq w(i,j))(即小区间取值(leq)大区间取值)
(2.)四边形不等式:(forall i leq i'< j' leq j,w(i,j)+w(i',j')leq w(i',j)+w(i,j'))
即中的红线总长(geq) 蓝线总长
如果(w(i,j))同时满足区间单调性和四边形不等式,那么(f(i,j))满足四边形不等式
令(S(i,j))为(F(i,j))在取到最优解时的决策点(k)
那么决策本身具有单调性,即满足(S(i,j)leq S(i,j+1)leq S(i+1,j+1))
用(j)代替(j+1)得到
(S(i,j-1)leq S(i,j)leq S(i,j+1))
转移方程变为
(F(i,j)=min(F(i,k)+F(k+1,j))+w(i,j); (S(i,j-1)leq kleq S(i+1,j)))
可以证明,他将时间复杂度降到了(O(n^2))
什么时候使用四边形不等式?
只需要牢记公式
(S(i,j-1)leq S(i,j)leq S(i,j+1))
考试时可以打一张决策表看是否满足上面式子,满足可以使用四边形不等式
(1.)序列(DP)有时可以使用四边形不等式优化,但仅仅是常数优化
(2.)需要注意四边形不等式仅针对求最小值的情况
(3.)注意(S)数组(下标取值范围)需要初始化,(S[i][i]=i)
树形(DP)
题目清单:
状态设计
对于这一类(DP),一般设计两维状态,(DP[i][j])表示当前节点为(i),子节点或相邻节点的状态为(j)时的答案
状态转移
(1.)树形结构天然的(dfs)序保证了更新当前节点时子节点已经被更新完毕,所以就把(DP)过程放到(dfs)遍历中即可,注意初始化。
一般每搜索一颗子树回溯回来就更新答案,最后在循环外面向上回溯前自己更新自己答案
(2.)当然还有种状态转移是需要所有相邻节点的状态,对于这种问题直接(for)循环枚举所有点即可,但要注意枚举顺序,是否会出现状态没有的情况,用多维循环内层枚举当前点,最内层枚举相邻点
状压(DP)
题目清单
状态设计
状压(DP)是一类非常特殊的(DP),基本思想是把(DP)的状态压缩为二进制等,以减小空间且保证转移正确性
一般看到一个题数据范围是(n<=18)且是动态规划,则大概率是状压(DP)
状态设计时,一维用二进制记录状态,一般在((1<<n)-1)左右,如果需要可以设计另一维表示当前位置,对于很多与选择顺序无关的题目这一维可以省略。
也有时候第二维可以分割为另一个(DP)数组,大大优化空间
状态转移
(1.)位运算的使用:位运算可以提高效率且减少代码复杂度
s<<=1 //左移*2
s>>=1 //右移/2
(s&(1<<i-1)) //判断第i位是不是1
s|=(1<<i-1) //把第i位设置成1
s&=~(1<<i-1) //把第i位设置成0
s^=(1<<i-1) //把第i位取反
s&=s-1 //把s最靠右的1去掉
s&(-s) //返回最靠右的1代表的值
for(s0=s;s0;s0=(s0-1)&s) //依次枚举s的子集
(1<<n)-1 //n位全是1的状态
(s&((1<<n)-1)) //只保留s的前n位,避免越界
(2.)状压(DP)转移
重点在于考虑记录哪些状态
一般一维枚举前一个状态(一般是最外层),另一维枚举转移什么(一般是一个值);少数情况第二维枚举接下来状态,因为这样复杂度极大,一般需要剪枝;极少数情况很毒瘤,表面上看起来只能枚举两种状态,但可以拆到另外一个(DP)数组,变为第一种转移方式,比如装箱问题,要求数量最小,可以用一个(DP)记录最后一个箱子剩余空间,从而(O(Sn))转移,不过要注意辅助(DP)数组被更新的前提是正常(DP)数组能被更新,是一个依赖关系,也就是在保证第一答案最优时第二答案最优,这样是对的
传送门
(3.)优化问题
((1))很多无用状态可以不枚举,比如炮兵阵地问题,可以预处理每一行的合法情况装进(vector),(DP)时只需要枚举这些情况,大大降低复杂度。
((2))还有一类问题:愤怒的小鸟,在所有已有状态中枚举找到一个值,再枚举不在状态中的值,复杂度是(O(n^2)),但最优解与选择顺序无关,所以可以控制每次选择最小位置更新,复杂度(O(n))
((3))对于区间、字符串操作,可以压缩状态后连边,跑最短路求解
((4))与考虑顺序无关的(DP)可以优化掉一维,只与附近固定位置有关,可以使用滚动数组优化
一定要小心MLE!!!!!
数位(DP)
状态设计
一般数据范围为(10^{18})
通常使用多维,每维规模比较小。
如(dp[i][j][k][0/1])表示当前在第(i)位(从最低位到最高位编号),当前位置是(j),附近位置的状态是(k),是否满足题目要求条件时的方案数(一般记忆化搜索(k)表示前面状态,递推(dp k)表示后面状态,记忆化搜索还需要记录是否小于边界)
状态转移
数位(DP)一般有两种做法:(1.)递推计数,(2.)记忆化搜索,两者的核心思想:逼近法,逐位确定是一样的
逼近法(先讨论(1-x)的答案)
分两种情况讨论,一种是位数(<cnt),一种是和答案位数相同,一般不考虑前导零,第一种情况需要单独处理
对于第一种情况,位数小于边界位数,枚举的数一定小于边界,枚举位数是多少,第一位(in [1,9]),后面位置随便取数即可,直接计数。
对于第二种情况,逐位确定,首先最高位(in [1,a[cnt]))时一定是小于边界的,所以后面位置随便取,计数,然后确定最高位为(a[cnt]),处理下一位,最终逼近到边界(需要注意的是最后会停在(x-1),可以单独处理(x),或者提前把边界(+1))。每次都不卡到边界上,保证后面所有位置可以随意取值便于计数
下面以(65536)为例看一下如何逼近((x)表示该位置可以随意取数)
如果题目要求([L,R])的答案,直接求(ans[L-1],ans[R]),作差即可。
继续回到状态转移上
对于递推计数的方法,首先要预处理(dp)数组(本身是数位(DP)),再按照逼近法计数,状态多的时候预处理会很麻烦,需要边界(+1)
对于记忆化搜索,只需要边搜索边记录(DP)数组即可,对于这种方法,状态转移比较简单,码量小,比较容易思考,边界不需要提前(+1)
注意的问题
(1.)递推计数预处理时有时可以用前缀和等等方法优化
(2.)初始值为(-1)时,小心数组下标越界
(3.)看好题目范围中是否包含(0)
(4.)前导零情况单独处理