【算法】动态规划
【动态规划杂记】状态+转移
核心:划分阶段-状态表示-状态转移方程。
复杂度:状态数O(n^t),转移O(n^e),则称为tD/eD问题。
1.最优化问题和方案数问题常考虑DP,特定数问题不考虑DP。
2.断层思想:划分状态,从计算过的状态去答案,这就是无后效性。(eg.对于搜索树按高度划分状态)
3.记录思想:通过记录所有状态和状态转移达到最终状态。(有一种方式是把答案的要素都列入状态,然后考虑转移)
4.DP有三种形式:传统式,刷表法,记忆化,选择形式一般由方便程度决定。
5.DP可以和贪心结合来划分阶段,帮助DP解决最优性未知的问题。
6.为了转移,需要使状态表示方便转移(例如在状压基础上把最后两个点提出来单独放着,因为后面要用)。
6.奇怪的DP若感觉有依赖关系,要么考虑一动一不动,要么把状态表示出来。
7.邮局问题(x轴有n个村庄,选m个村建邮局,求所有村到邮局距离和最小):f[i][j]表示i村选j邮,枚举一段村属于一个邮局来转移,邮局建在这段村中点。这是一种典型的放缩,虽然算出来不一定是当前邮局分配下的正确答案,但是正确答案能被算到,不正确答案不会更优。
8.最长上升子序列:偏序 O(n log n)。(上帝选人:二分图不交叉最大匹配,一维排序一维LIS)
9.DAG上的动态规划:令f[i]为以i为起点的最长路——记忆化搜索
令f[i]为以i为终点的最长路——拓扑排序+DP(刷表)
10.单调队列优化:单调队列的作用是强制使h(i)和i同时具有单调性,从而可以同时处理坐标和最值相关的事情。
最常见的应用就是处理移动区间最值。
还有一个应用:对于条件ai>=bj找坐标最大,维护bj的单调队列。如果ai有单调性就可以直接找队头,否则二分单调队列。
收集了一个系统的教程:[dp优化]个人对dp优化的理解
11.几个套路:
①状态与值的互化
②和贪心的结合
③转化为矩阵
【斜率优化】
参考:斜率优化DP(文中有些小错误,维护的是下凸包)
★例题:【BZOJ】1597 [Usaco2008 Mar]土地购买
斜率优化题目的通法:
1.列式:列出决策优劣比较式(i阶段,j<k且k更优时),形如(y[j]-y[k])/(x[j]-x[k])<k[i],当k[]和x[]同时具有单调性时可以进行斜率优化,定义更满足比较式为优,维护从最优到最劣的决策队列。
2.决策:选取队头两个决策d(head,head+1),若d满足优劣式说明k更优,删除队头j,继续比较;若d不满足优劣式说明j更优,则j为当前最优决策。
3.入队:选取队尾两个决策d1(tail-1,tail)和当先决策与队尾决策的d2(tail,i),若d2优于d1则删除队尾,继续比较,直到d2劣于d1就把i入队。
解释:
1.对于每个决策点是二维坐标系中的一点,比较式为"<"时,只有构成下凸包的点有效(">"为上凸包)。
2.对于给定斜率k[i],需要找到凸包上斜率最接近的一条直线,恰好前一条线后面更优,后一条线前面更优,从而得到最优决策点。
3.当k[]和x[]同时单调时可以直接斜率优化,否则用CDQ分治维护。
【分治决策优化】解决决策单调性问题
参考:《浅谈决策单调性动态规划的线性解法》冯哲 2017集训队论文
条件:决策具有单调性。
复杂度O(n log n)
令h[i]为f[i]的决策来源。
对于h[n/2],有[1...n/2]从[1...h[n/2]]决策。
有[n/2...n]从[h[n/2]...n]决策
dp[i][j] 前i个数字,分成j段,最小平方和 f[0]=0; void solve(int l,int r,int L,int R){ if(l>r||L>R)return; int mid=l+r>>1; int where,value=INF; for(int i=L;i<=R&&i<mid;i++){ if(g[i]+(sum[mid]-sum[i])*(sum[mid]-sum[i])<value){ value=g[i]+(sum[mid]-sum[i])*(sum[mid]-sum[i]); where=i; } } f[mid]=value;//mid 从 where 决策 solve(l,mid-1,L,where);solve(mid+1,r,where,R); } for(int j=1;j<=k;j++){ memcpy(g,f,sizeof(f)); memset(f,0x3f,sizeof(f)); solve(1,n,1,n); /*for(int i=1;i<=n;i++) for(int x=1;x<i;x++){ f[i]=min(f[i],g[x]+(sum[i]-sum[x])*(sum[i]-sum[x])); }*/ } k*n*n k*n*logn
分治决策优化解决决策单调性问题十分套路化而且简便好写。
在此之前解决决策单调性问题的方法:《用单调性优化动态规划》,可以忽略。
【背包DP】取和不取的问题可以考虑转成背包。
<01背包>
$$f_{i,j}=max\{f_{i-1,j},f_{i-1,j-w_i}+c_i\}$$
for i:=1 to n do
for x:=m downto w[i] do {不能写成for x:=w[i] to m}
if f[x-w[i]]+c[i]>f[x] then {f[x]表示重量不超过x的最大价值}
f[x]:=f[x-w[i]]+c[i];
<完全背包/无限背包>
$$f_{i,j}=max\{f_{i-1,j},f_{i,j-w_i}+c_i\}$$
for i:=1 to n do {f[x]表示重量不超过x公斤的最大价值}
for x:=w[i] to m do {不能象0/1背包一样用反向}
if f[x-w[i]]+u[i]>f[x]
then f[x]:=f[x-w[i]]+u[i];
<有限背包>【HDU】2191 多重背包问题 二进制分组(一直减2^(k++)直到不能减的余数当作最后一个)
<二维费用背包>相当于两个容量,多加一维就行。物体总个数限制也属于此类。
加一维限制条件来满足原来的经典转移而已。
<分组背包>最后枚举同组物件来限制一组一件,即对于f[v]进行全组决策完再转移。
<Bitset优化存在性背包>f[i]表示是否存在重量为i的方案。
for(int i=1;i<=n;i++){ for(int j=m;j>=a[i];j--){ f[j]|=f[j-a[i]]; } } ////////// for(int i=1;i<=n;i++){ f=f|(f<<a[i]); }
【区间DP】
按区间长度从小到大枚举,通过转移体现最优子结构。
回文串拥有很明显的区间子结构特征,当i+1 > j-1时也是有意义的,空串也是一个回文串
for i:=1 to n do f[i,i]:=0;{初始化} for p:=1 to n-1 do //合并的堆数p: 阶段 for i:=1 to n-p do //枚举状态: begin j:=i+p; f[i,j]:=maxlongint; for k:=i to j-1 do {枚举决策} f[i,j]:=min(f[i,j],f[i,k]+f[k+1,j]); f[i,j]:=f[i,j]+s[j]-s[i-1]; end;
【树型DP】
<转二叉树>左孩子右兄弟表示法
read(i,j);//i:父;j:子 if left[i]=0 then left[i]:=j else begin right[j]:=left[i]; left[i]:=j; end;
<1>在二叉树进行DP
状态转移方程模型:f[i,j]:=f[LC,k]+a[i]+f[RC,j-k](f[i,j]表示以i为根的子树选取j个特殊节点的值)
<2>树的经典问题
1)树的直径:记录最深和次深。
有向树的直径:f[i]表示往上走的最长链,g[i]表示向下走的最长链,经过i的最长链等于f[i]+g[i]。
(紫书的题之所以采用二分答案,是因为局部最优≠全局最优,我们是要让最长链(即所有链)严格小于一个值,不是让某一段链尽可能小。)
2)树上所有点的最远点:
<1>第一次treedp,得出f[i]表示i为根的子树的最深叶子路径,g[i]表示次深叶子路径。
<2>第二次treedp,对于节点x,传下来s表示向上的最长路,所以s[x]=max(s,f[x])。
遍历x儿子son[x]时s的传递:若f[son[x]]+1=f[x]则s=g[x]+2否则s=f[x]+2。
套路:当需要统计节点向上信息时,第一遍先统计向下信息,并且每个节点集成儿子的信息之和,第二遍将传下来的信息和(节点信息减去儿子信息)传给儿子作为向上信息。
3)树的重心:【算法】树
<3>在多叉树上进行DP
当题目需要根据取或不取进行决策时,就需要在数组上多加一维01表示是否取根。
状态转移方程模型:f[i,j]:=f[son[i,1],j1]+f[son[i,2],j2]+...+f[son[i,x],jx](j1+j2+..+jx=j)
分配j的方法是背包:对于节点i,将一个儿子的信息与节点合并后,再转而处理下一个儿子,当信息与节点数有关时(如特殊节点数),枚举根信息上限(之前已合并了的子树结点总数+当前子树结点数,记为now_size)和当前子树信息上限(当前子树结点数)。
for(son[x])
for(i=0...min(now_size,maxm))
for(int j=0...min(size[son[i]],maxm))
f[x][i]=min(f[x][i],f[x][i-j]+f[son[i]][j])。
背包千万不要枚举满,当信息与结点数有关时按子树结点数枚举,总复杂度摊下来是O(n2)。
树型DP几乎都是DFS。
【数位DP】专题链接
【插头DP】
讲解:【Ural】1519. Formula 1 插头DP 哈密顿回路问题(单回路问题必须在最后一有效格闭合,最后一格取答案)
在特殊的题目,只要改变状态表示就可以记录很多信息,从而满足题目的要求。
例题:
1.【BZOJ】2331: [SCOI2011]地板 插头DP 特殊覆盖问题
2.【BZOJ】2310: ParkII 插头DP 简单路径问题(增加一个状态位记录有0~2个独立插头)
下面例题和内容参考kuangbinの博客:
3.FZU 1977 Pandora adventure:此题也是单回路数问题。但是格子有了三种:障碍格子,必走格子和选择性经过格子。
题解:单回路问题的特点是必须在最后一格闭合,此题最后一格不确定,所以增加一个状态位来记录是否形成回路。
4.POJ 3133 Manhattan Wiring:格子中有两个2,两个3.求把两个2连起来,两个3连起来。求经过总的格子数的总和减2. 两条路径不能交叉。有障碍格子。非障碍格子最多经过一次。
题解:插头记录三种状态:没有插头、2号插头、3号插头。
指定起点和终点的问题,只要在起点和终点处特殊处理独立插头的问题即可。
5.HDU 4285 circuits:求K个回路的方案数。而且不能是环套环。
题解:增加一个状态位来记录形成的回路个数。在第x列闭合一个回路时,如果左边的插头数(<x-1)是奇数就是无效状态。
这是因为每条回路一定有偶数个插头扎在轮廓线上。即使有环套环套环,这种状态也会在闭合第二外层的环时被判定为无效。
6.Uva10572 Black&&White:给定n*m的棋盘,有些格已经染了黑色或白色,要求给所有格染色使得黑色和白色各自连通且不存在2*2的同色矩形。
题解:判断2*2只需要多记录一位状态,主要解决黑白色各自连通的问题。枚举过第x格时,第x格上方的方格马上会退出轮廓线表示范围,如果轮廓线中不存在它的连通块,这种颜色以后就不能再出现了。出现这种情况是直接判断剩下的方格是否成立计算答案即可。
之所以需要这么做,是因为普通路径问题有插头的存在,不可能丢失路径。但当跳过第x格时,第x格上方的方格就和状态无关了。
还有一个交叉状态优化:如果独立连通块abcd(不嵌套),ac一色,bd另一色,则无解。
留坑:BZOJ1494(论文第四部分思想)+ZOJ3256(kuangbin的矩阵加速题目)
【状压DP】
1.预处理:如果过程中都是重复的转移,预处理两个状态之间的转移是否合法。
例题:【BZOJ】1087: [SCOI2005]互不侵犯King 预处理上下两行的状态是否合法。
例题:【BZOJ】2004: [Hnoi2010]Bus 公交线路 状压DP+矩阵快速幂 每次转移都是调用两个状态判合法,直接预处理后可以矩阵快速幂加速。
状态的设置:观察转移过程的需要以及数据的范围。
2.转移方式:
(1)递推法,从前面取答案
(2)刷表法,加后面的答案。还可以把有加的才加入队列,简化状态数(相当于广搜BFS)。
(3)记忆化搜索,类似刷表法。
3.二进制操作技巧(分清 ”或|“ 和 ”与&“ 的区别,善用lowbit(x)取最低位的1,善用(x+1)和(x-1))
(1)单个位变0:先”或“成1,再减掉。
(2)枚举所有集合的子集:for(int s=S;s;s=(s-1)&S)
(3)统计1的个数:不断lowbit(x)后减掉。
(4)最右的连续0/1或第一个0/1:参考状压DP初探·总结,都是利用x+1影响最右连续1,x-1影响最右连续0来实现的。
4.例题:
(1)铺地砖:每层影响下一层的状态通过本层的铺地砖来得到,这个铺地砖的过程通过dfs整行来实现比较方便,参考:铺地砖|状压DP练习。
(2)TSP问题:【CODEVS】2800 送外卖
-------------------------------------------------下面待整理-----------------------------------------------------------
(3)下面笔记比较乱,不舍得删,想看就看吧。
UVA 10817 m教师n求职者给出每人工资c课程表,求最少支付使每节课至少两个老师教。
因为至少两个老师,S0(未教,不用记忆化,s1和s2确定时s0就唯一对应了),S1(一个老师),S2(两个老师)。
也可以压成一个状态二进制16位或者四(三)进制8位,只不过操作稍显繁琐而状态数实际上是一样的。
d[i][S1][S2]表示已经考虑了前i个人取舍的最小花费(注意是考虑了,不是只考虑)。
d(i,S1,S2)=min{d(i+1,S1',S2')+c[i],d(i+1,S1,S2)}对应从取和不取(i≥m)转移过来。
为什么是i+1?若i从i-1转移,也就是若取i则从缺了i的状态状态转移过来,状态处理将十分不便,
方便地在二进制中体现i的取舍的方法显然是决策取1或取0然后把这个结果送给i+1去继续决策。
可以想象出状态转移是一颗从(0,0,0)发散出去最终到达n+m的树,为了方便记忆化,用d[i][S1][S2]记录对应子树的值,令d[i][S1][S2]表示前i决策完毕后还需要支付给后面的n+m-i个老师的最小工资最合适。
这也体现了记忆化搜索的本质是记录下搜索树的子树值在子树根节点上。
这种有点01背包性质(取或不取)的状压DP,一般为了方便处理都会采用记忆化搜索,并且i+1向i转移。
不过也可以采用刷表法,将影响到的下层状态放入队列。
<5>UVA1252状态设计也十分巧妙,s表示已询问,a表示已确认具备,转移中取最大值,通过预处理边界条件降低复杂度。
<6>UVA1412不必为复杂的描述困扰,实际上都是熟悉的算法。
插头DP的优越性在于:对于一些问题来说(尤指棋盘类问题),一行的状态有许多是前缀相同的,那么决策情况也相同,这时轮廓线就可以把相当的一起算,大大减少状态转移复杂度。
所有其实很多题目都可以用插头DP加速,包括摆砖、骑士共存等棋盘问题。
插头DP一般只记一行,但不得已也可以记两行(骑士共存)等,如果可以传承标记的话也可以压成一行(但是状态数复杂度是一样的!)。
【未来决策DP】
——论文《对一类动态规划问题的研究》笔记
未来决策DP:★将当前决策对未来的影响都在现在计算,然后未来只要按正常决策计算就可以了
1.第一类问题,考虑代价与时间的一致性,将本来的时间维算成代价(枚举总收益),省略时间维。
当然可以把当前收益和未来代价一起算。
核心是当前决策对未来的影响都在现在计算,然后未来只要按正常决策计算就可以了。
2.题意:给定1~n的排列,要求排序,每次操作可以将一个数从位置i移动到位置j,代价为i+j,求总代价最小。
结论:按编号从大到小处理,对于每个x,有1.移动到x+1前一个位置,2.不移动,x和x+1之间的数移动到x之前。
采用一个重要操作:将移动到x+1前一个位置视为移动到x+1的位置,简化运算!
令f[i][j]表示第i个数移动到位置j的最小代价。pos[x]表示数x的原位置。
为了转移时计算x的新位置,采用相对转绝对的方法。因为<x的数字均为处理,假设>x的数都在后面,则
c(p,x)表示之前位置在p的x至现在的新位置(代价),等于1~p中小于x的数字个数+1。
枚举x+1的位置p2,f[x][p2]=f[x+1][p2]+c(pos[x],x)+c(p2,x+1)-1。x和x+1位置前后无关。
再处理x不动的情况,k=pos[x]+1~p2-1的数字未来一定要多移动x-s[k]的代价。(s[k]表示原序列k位置的数字,x在前则x~s[k]-1都在前)
f[i][pos[i]]=min{f[i+1][p2]+∑(x-s[k]),x>s[k]}
和第一题思想一致,将当前决策对未来的影响在当下计算,未来算的时候按正常决策计算。