初级DP
0. DP的概念与设计和实现
概念:DP从本质上讲是图论问题的中的一种,DP的每一种状态所对应的便是一张图上的点,转移对应的便是图上的边。
如果是求最值,那便是图论中的最短路或最长路;如果要求方案数,那便是图论中的路径统计问题。
设计:DP的设计有三大要素:状态,转移方程,初始化。
实现:DP的实现通常是三种方式,从当前状态向可能状态转移,从可能前继状态向当前状态转移,记忆化搜索。通常根据实现的难易来选择。
1. 入门DP
1.数字三角形
我们先确定状态 \(F[i][j]\)表示到达当前位置\((i,j)\)所累积的最大的数是多少。
接下来考虑转移\(F[i][j]=max(f[i-1][j],f[i-1][j-1])+w[i][j]\).
不需初始化。
时间复杂度\(O(n^2)\) 代码
可以线段树求动态区间前缀最大值优化到\(O(nlogn)\)具体请看 初级DP优化(鸽了
变式1.求在模100后的最大值。
当DP条件变多时,我们便考虑给DP多开一维来记录状态\(F[i][j][k]\)表示当到达位置\((i,j)\)时模\(100\)等于\(k\)是否可能\(1\)表示可能\(0\)表示不可能。
于是我们得到转移方程\(F[i][j][k]->F[i+1][j][(k+w[i+1][j])\%100]andF[i+1][j+1][(k+w[i+1][j+1])\%100]=1\)
最后扫描最后一行每一个位置\(1-k\)是否合法即可。
时间复杂度\(O(n^3)\)。
变式2.求必经某个点限制下原问题的答案。
只需先算\((1,1)\)到该点的最大值再算该点到最后一排的最大值即可。
变式3.求不经过某个点限制下原问题的答案。
只需给不能经过的点初始化一个负无穷的值即可。
2.最长上升子序列
设计状态\(F[i]\)表示以i位置结尾的最长子序列长度
得到转移\((a_j>a_i)\)时\(F[i]=max_{j\in[1,i)]}(F[i],F[j]+1)\)
初始化\(F[i]=1\)
时间复杂度\(O(n^2)\) 代码
考虑记录方案数
新开一个g数组
if(f[j] + 1 > f[i]) { f[i] = f[j] + 1; g[i] = 0; pre[i] = j;}
if(f[j] + 1 == f[i]) g[i] += g[j];
如果第一行执行那么第二行也必然执行 这样写才能达到我们想要的效果
考虑记录一种方案
如上方代码中记录的前继数组pre 输出时参考下述代码
int z[], cnt;
do
{
z[++ cnt] = p;
p = pre[p];
} while(p != 0);
reverse(z + 1, z + 1 + cnt);
一些例题
1.滑雪
考虑\(F[i][j]\)表示到位置\((i,j)\)最多滑雪步数
转移即为上下左右四个方向取\(max\)向当前位置转移并\(+1\)
初始化所有点为\(1\)
这样对吗? NO。 因为我们的转移需要从步数少的地方向步数多的地方转移(这类似我们要先以高度进行一个拓扑排序)然后再判断当前位置上下左右哪个位置高度大于该处才能转移到该处
时间复杂度为check ans的\(O(nm)\) 代码
2.乌龟棋
考虑状态\(F[x][a][b][c][d]\)为当前在x位置使用了a张1牌b张2牌...
显然x可以通过1+a+2b+3c+4d得到 于是便可以压掉
转移便是当前位置使用1或2或3或4向后转移即可
时间复杂度\(O(max种类牌数^4)\) 代码
3.导弹拦截3
再开一维记录这发导弹是比上一发底还是高 特别注意初始化时第一颗导弹必须是比上一颗高
时间复杂度\(O(n^2)\) 代码
2.区间DP
区间DP状态前两维通常为\(F[i][j]\)表示区间\(i->j\)的某种值
转移通常为枚举一个断点\(k\)来更新\(F[i][j] = F[i][k] ? F[k+1][j] + ?\) 其中\(k\in[i,j)\)
转移时我们要先从小到大枚举一个len再枚举l这样才能保证我们是从小区间转移到大区间的
初始化一般为\(F[i][i]=w[i]or0\)
常见技巧的有环形需要我们开多倍长度断环为链
1.合并石子
按上述描述我们很容易完成这道题
设状态\(F[i][j]\)表示将区间\([i,j]\)内石子全部合并成一堆的最大得分
枚举断点\(k\)转移\(F[i][j]=max(F[i][j],F[i][k]+F[k+1][j])\)
初始化\(F[i][i]=1\)
最小值只需将转移方程中的\(max\)改为\(min\)即可
时间复杂度\(O(n^3)\) 代码 其实细节还是不少的,不是很好写。
2.Brackets
一样的设\(F[i][j]\)表示\([i,j]\)最多匹配括号数
转移需要分两种情况 一种是\(F[i][j]=max(F[i][j],F[i][k]+F[k+1][j])\)
当\(s[l]匹配s[r]\)时 \(F[i][j]=max(F[i][j],F[i+1][j-1]+2)\)
时间复杂度\(O(n^3)\)代码
3.Multiplication Puzzle
注意到左右端点的数不能删除 所以我们设状态\(F[i][j]\)表示\((i,j)\)内的数被删干净的最小代价和
转移\(F[i][j]=min(F[i][j],F[i][k]+F[k][j]+w[k-1]*w[k+1])\)
注意前后都是k因为根据我们的状态意义左右端点是没有被删的
时间复杂度\(O(n^3)\)代码
注意枚举断点k的时候\(k\in(l,r)\)左右都是开区间 理由同上
4.Palindrome subsequence
设\(F[i][j]\)表示\([i,j]\)内回文子序列的数量
那么得到\(F[i][j]=F[i+1][j]+F[i][j-1]-F[i+1][j-1]\) 根据容斥原理这样可以将新的方案统计进来
还要注意 如果\(s[i]==s[j]\)那么总方案还会多上\(F[i+1][j-1]+1\)即所有原来的回文串左右拼接上和左右两个字符组成一个回文串
时间复杂度\(O(n^2)\)代码
ex:如果是回文子串它是一道什么题呢?
5.248 G
差不多的做法,但注意特判不能用不合法状态转移下去
时间复杂度\(O(n^3)\) 代码
6.*矩阵取数游戏
7.*[CQOI2007]涂色
8.*[SCOI2003]字符串折叠
9.*[NOIP2006 提高组] 能量项链
10.*[SDOI2008] Sue 的小球
11.N边形三角划分
设计状态\(F[i][j]\)表示将\([i,j]\)划分完的最优解
得到转移\(F[i][j]=max(F[i][j], F[i][k]+F[k][j]+w[i]*w[j]*w[k])\)
3.背包DP
--2023.6.3-6.4凌晨时阅读背包九讲后部分重构
1.01背包
最简单的背包 直接给出转移方程\(F[i][j] =max(F[i-1][j],F[i-1][j-v_i]+w_i)\)
for (int i = 1 ; i <= n ; ++ i)
{
for (int j = v[i] ; j <= V ; ++ j)
{
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}
转移只涉及\(i-1\)考虑滚动数组滚掉第一维\(F[j]=max(F[j],F[j-v_i]+w_i)\)
需要注意枚举\(j\)时要从大到小枚举!
这样才能保证\(F[i][j]\)是由\(F[i-1][j-v_i]\)更新来的而不被\(F[i][j-v_i]\)所影响
for (int i = 1 ; i <= n ; ++ i)
for (int j = V ; j >= v[i] ; -- j) f[j] = max(f[j], f[j - v[i]] + w[i]);
初始化
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了\(F[0]\)为\(0\),其它\(F[1..V ]\)均设为\(−∞\),这样就可以保证最终得到的\(F[V]\)是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 \(F[0..V ]\) 全部设为 \(0\)。
这是为什么呢?
可以这样理解:初始化的\(F\)数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为\(0\)的背包可以在什 么也不装且价值为\(0\)的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为\(-∞\)了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为\(0\),所以初始时状态的值也就全部为\(0\)了。
这个小技巧完全可以推广到其它类型的背包问题,后面不再对进行状态转移之前的初始化进行讲解。
常数小优化
\(j\)的枚举范围其实可以变为\(V->max(V-\sum_{i=1}^n w[i],v[i])\)
2.完全背包
每个物品可以选无数次
朴素的想法是在原来的基础上再枚举一个选择次数 但这样的复杂度是\(O(n^3)\)
但其实我们直接用\(F[i][j-v_i]+w_i\)来更新\(F[i][j]\)即可这样就相当于我们把这件物品取\(1、2、3......\)都考虑进去了
for(int i = 1 ; i <= n ; ++ i)
for(int j = 0 ; j <= V ; ++ j) f[i][j] = max(f[i - 1][j],f[i][j - v[i]] + w[i])
一样使用滚动数组滚掉第一维度 \(j\)要正向枚举
for(int i = 1 ; i <= n ; ++ i)
{
for(int j = v[i] ; j <= V ; ++ j) f[j] = max(f[j], f[j - v[i]] + w[i]);
}
3.多重背包
每个物品有固定的个数\(k_i\)
一种朴素的想法是对每个物品建成它个数个的物品,这样就被转化为了普通的01背包问题
但这样的时间复杂度是\(O(V\sum_{i=1}^nk_i)\)
考虑倍增分组(很多地方叫它二进制分组 其实它应该叫倍增分组) 即将每个物品分成1、2、4、8....分组
为什么这样分呢?因为这些数的二进制恰好为1、10、100、1000这样可以保证可以组合出选这个物品任意个数的所有方案
复杂度变为\(O(V\sum_{i=1}^nlog_2k_i)\)
如果改为可行性多重背包(指求将背包填满的方案数,不考虑价值)有一种使用单调队列的\(O(Vn)\)复杂度的做法 等我以后写在初级DP优化里。
4.混合背包
上面几种混在一起
第一层\(i\)的循环不变,第二层\(j\)的循环考虑哪种物品属于哪个情况就跑一遍哪个转移。
for i <- 1 to N
if第i件物品属于01背包
ZeroOnePack(F, v[i], w[i])
else if第i件物品属于完全背包
CompletePack(F, v[i], w[i])
else if第i件物品属于多重背包
MultiplePack(F, v[i], w[i], N)
5.二维费用背包
再开一维记录第二个费用。
其余均一样。
有总选择物品数量上限的背包
这其实就相当于每个物品有了一个第二维费用1。
6.分组背包
将每一组当成一个物品跑01背包。
设\(F[i][j]\)表示前\(i\)组花费\(j\)的体积能获得的最大价值,则有:
\(F[i][j]=max(F[i-1][j],F[i-1][j-v_x]+w_x)|x\in i\)
for(int i = 1 ; i <= n ; ++ i)//枚举哪一组
{
for(int j = m ; j >= 0 ; -- j)
{
for(int x = 1 ; x <= cnt[i] ; ++ x) f[j] = max(f[j], f[j - v[x]] + w[x]);
}
}
三层(主要是体积反着枚举)循环的顺序保证了每一组内的物品最多只有一个被选
一个显然的优化
如果一个物品体积更小价值更大,那么一定比它劣的物品就可以被删去了
(如果一个人比你小还比你强,那么你就没用了)
依赖背包(放到树形DP中讲)
7.泛化物品
假如一个物品所对应的价值为一个函数\(h()\)你花费\(V\)的体积便能获得\(h(V)\)的价值
上述几种背包都是泛化物品的简单形式
如01背包里的物品便是\(h(v)=w,h(other)=0\)的泛化物品
一个物品组自然也可以看作一个泛化物品 该物品的\(h\)函数为,若不存在体积为\(v\)的物品\(h(v)=0\)否则\(h(v)\)取体积为\(v\)的物品中价值最大的一个\(w\)
如果给定了两个泛化物品\(h\)和\(l\),用一定的体积怎么取得最大的价值呢?
其实只要枚举这个体积是如何分配的即可
\(f(v)=max(h(k)+l(v-k))|k\in[0,v]\)
可以看出\(f\)是一个由\(h\)和\(l\)决定的定义域为\([0,V]\)的函数,所以\(f\)也是一个泛化物品,是由泛化物品\(h\)和\(l\)决定的泛化物品
所以求解任何背包问题其实都是在讲所有泛化物品合成一个泛化物品
只不过这个函数可能会有很多限制
8.求一种方案
类似普通DP记录方案的方法
拿\(01\)背包举例 转移方程\(F[i][j]=max(F[i-1][j],F[i-1][j-v_i]+w_i)\)
开一个\(G[i,j]\)表示当前\(F[i,j]\)是由前一项还是后一项\((0/1)\)推过来的
输出时大概这样写
i <-N
j <- V
while i > 0
if(G[i,j]) =0
printf 未选当前物品
else printf 选了当前物品, j <- j-v[i]
i <- i -1
字典序最小
DP时从后往前推
即\(i\)要从\(n\)到\(1\)枚举 这样更改后的子问题才能保证字典序最小(这里体积的枚举顺序无所谓,因为没有进行空间压缩)
输出方案时才能从前往后推
9.求方案数
只需将Max改为Sum即可
10.求最优方案数
分别根据转移时是取max前面一项还是后面一项转移\(G\)数组即可
\(G[0][0]=1\)
因为体积为\(0\)时什么都不选是一种最优方案。
11.求第k优解
再开一维表示当前状况的第\(k\)优解
复杂度相应的多乘上了一个\(k\)
根据题目对不同解的定义来判断是否有相同的值存在第三维中
一些例题
1.*[HEOI2013]Eden 的新背包问题
4.树形DP
基于树的DP 通常状态为\(F[i]\)表示以\(i\)为根的子树的某些值
DP方法为从叶子节点不断向上做聚合 实现方法为DFS
1.树的直径
设状态\(F[i][0]\)和\(F[i][1]\)分别表示\(i\)距离它子树内所有点的最远距离和次远距离是多少
注意上述最远和次远是对两个儿子而言 不能在一个儿子里
所有转移时取儿子里的最大和次大\(+1\)即可
时间复杂度\(O(n)\) 代码
记录节点
转移时记录最长和次长分别是哪个儿子即可
2.树上任意两点距离和
即求\(\sum_{i=1}^n\sum_{j=1}^ndis(i,j)\)
设\(F[i]\)表示以\(i\)为根的子树内所有点的路径和\(g[i]\)表示以\(i\)为根的子树内所有点到\(i\)的距离和\(size[i]\)表示以\(i\)为根的子树大小
从点的角度出发
转移为\(F[i]=\sum_{p\in i}F[p]+(g[p]+size[p])*(size[i]-size[p])\)
前面一个括号指的是以\(i\)为根的所有子树内的点到\(i\)的距离和,后面一个括号指的是以\(i\)为根的子树除掉\(p\)后剩下的子树大小
从边的角度出发更为简单
只需DP算出\(size[i]\)
\(ans=\sum_{i=1andi不是根节点}^nsize[i]*(n-size[i])*2\)
因为起点终点对换也要统计进答案,所以最后要\(*2\)
3.求树的最大独立集
设\(F[i][0/1]\)表示以\(i\)为根的子树内最大独立集是多少\(0/1\)分别表示\(i\)不选/选
易得到转移
\(F[i][1]=\sum_{p\in i}F[p][0]\)
\(F[i][0]=\sum_{p\in i}max(F[p][0],F[p][1])\)
初始化叶子节点\(F[i][1]=1\)
时间复杂度\(O(n)\) 代码
4.皇宫看守
设\(F[i][0/1/2]\)表示以\(i\)为根的子树内的全部点被守护且\(0/1/2\)分别表示\(i\)点是被儿子/自己/父亲守护的
得到两个显然的转移
\(F[i][1]=\sum_{p\in i}min(F[p][0], F[p][1], F[p][2])\)
\(F[i][2]=\sum_{p\in i}F[p][0]\)
发现\(F[i][0]\)的转移需要查询\(i\)的儿子中必选一个放士兵的最小值
所以我们再开一个DP
设\(g[k][0/1]\)表示前\(k\)个儿子中\(0/1\)(选/没选)的最优解
转移为
\(g[k][0]=g[k-1][0]+f[k][0]\)
\(g[k][1]=min(g[k-1][1]+f[k][1],g[k-1][1]+f[k][0],g[k-1][0]+f[k][1])\)
时间复杂度\(O(n)\) 代码 (你猜我调了多久。)
5.依赖背包
必须要先选当前点的物品才能选其儿子的物品
设\(F[i][j]\)表示\(i\)的子树内用了\(j\)的体积的最大价值
叶子节点初始化为\(F[i][0]=0,F[i][v_i]=w_i,F[i][其他]=-INF\)
再设\(g[i][j]\)表示前\(i\)个儿子用掉了\(j\)的体积的最大价值
初始化为\(g[0][0]=0,g[0][其他]=-INF\)
转移时枚举第\(i\)个儿子花费的体积
\(g[i][j]=max_{k\in[0,j]}g[i-1][j-k]+F[p_i][k],p_i是第i个儿子\)
然后更新\(F\)
\(F[i][j]=g[r][j-v_i]+w_i,r是儿子总数\)
其实就是在儿子向父亲更新时对所有儿子做了一个01背包
然后对所有树根建一个虚拟源点\(0\)号点
对\(0\)号点的所有\(F\)求\(max\)即可
因为题目限制的体积为10的倍数,枚举时按10加或减即可
时间复杂度\(O(<n^2m)\) 代码
ps:由此可以看出稍微复杂的树形DP通常是在转移时需要再开一个DP去计算转移
6.*[JSOI2018] 潜入行动
7.*P2889 [USACO07NOV]Milking Time S
5.数位DP
一般是求\([l,r]\)中有多少满足条件的数
转化为求\([0,r]-[0,l-1]\)
状态设计为\(F[i][0/1]\)表示当前填到第\(i\)位置是 小于/等于 x的数有多少个
假设x有\(k\)位 初始化为\(F[k+1][1]=1\)
1.求\([L,R]\)中有多少个数
我们用数位DP来做这道题
考虑转移枚举下一位填什么
\(F[i-1][0]+=F[i][0]\)
当枚举的数小于原数当前位置的数时\(F[i-1][0]+=F[i][1]\)
等于时\(F[i-1][1]+=F[i][1]\)
时间复杂度\(O(a+b)\)代码
一种优秀的写法是
UF(i, cnt + 1, 2)
{
F(j, 0, 1)
{
F(r, 0, 9)
{
if(j == 1 && r > y[i - 1]) continue;
f[i - 1][j != 0 && (r == y[i - 1])] += f[i][j];
}
}
}
2.求在\([L,R]\)中的数的数位之和
设计为\(G[i][0/1]\)表示当前填到第\(i\)位置是 小于/等于 x的数的数位之和为多少
转移时枚举下一位填什么
\(G[i-1][0]+=G[i][0]+r*F[i][0]\)
当枚举的数小于原数当前位置的数时\(G[i-1][0]+=G[i][1]+r*F[i][1]\)
等于时\(G[i-1][1]+=G[i][1]+r*F[i][1]\)
发现就是对原来F的转移替换为G的转移然后再加上当前位置填什么乘上前面有多少个数
UF(i, cnt + 1, 2)
{
F(j, 0, 1)
{
F(r, 0, 9)
{
if(j == 1 && r > y[i - 1]) continue;
f[i - 1][j != 0 && (r == y[i - 1])] += f[i][j];
g[i - 1][j != 0 && (r == y[i - 1])] += g[i][j] + r * f[i][j];
}
}
}
3.*[SCOI2009] windy 数
再开一维记录上一位选了什么。
前导\(0\)特殊处理
调不出来了。
4.[SCOI2011]镜像拆分弱化
原题是假题。
假如我们去掉K进制限制。
设\(F[i][j][k]\)表示填到第\(i\)位,前部分 等于/小于\(1/0\) 后半部分 大于/等于/小于\(2/1/0\)
注意后半部分是有可能大于原数的 只要前半部分小于即可
5.*求在[L,R]中的满足各位数字之积为K的数有多少个
设\(F[i][j][k]\)表示填到第\(i\)位,前部分 等于/小于\(1/0\) 乘积为\(k\)
但是\(k\)很大 怎么办
考虑到是一位一位乘,所以质因子只有\(2,3,5,7\)四个
分别开一维记录有几个即可
真的做完了吗?
考虑\(k\)为\(0\)的情况
要新做一个DP是填到第\(i\)位,前面有没有填过\(0\)
前导\(0\)特殊处理
6.[l,r]中找一个x使各位之间差值绝对值和最大
发现这次并不是求有多少数满足条件
所以我们要改一改我们的DP
设\(F[i][j][k][l]\)分别表示当前填到第几位,当前的数是小于/等于\(r\),当前的数是等于/大于\(l\),上一位填的是\(l\)
枚举这一位填什么转移即可
6.排列DP
一般的状态设计是\(F[i][j]\)表示\(1-i\)都已经放进序列,满足某种限制的个数是\(j\)的方案数是多少
根据题目思考一下是从小到大放还是从大到小放
编号一般从\(0\)开始编号比较方便
1.\(1-n\)排列中有多少个排列的逆序对的数量是偶数
从小到大放
设\(F[i][j]\)表示\(1-i\)已经放好,有\(j\)个逆序对
转移时枚举放在哪里即可,因为从小到大放,所以放到编号为几的位置逆序对就会多几个。
发现只要求偶数,所以第二维可以改成记录奇偶
其实这个题目是有结论的,答案为\(n!/2\)
2.*LEMOVIE: Little Elephant and Movies
考虑从大到小放,比较容易保证题目的要求
转移考虑只有当前数放在最前面才能使激动值增加
设已经插入\(x\)个数,第\(i\)大的有\(y\)个
\(f(i,j)=f(i-1,j)*\binom{x+y-1}{x-1}*{y!}\ +\ f(i-1,j-1)*\binom{x+y-1}{x}*{y!}\)
3.*\(1-n\)的排列中,有多少最长上升子序列长度\(≤2\)
7.状压DP
枚举子集的方法
for(int i = x ; ; i = (i - 1) & x)
{
do sth.
if(!z) break
}
1.异或背包
\(F[i][j][k]\) 物品选择压缩成的状态/上一个选的物品/当前所用体积 能获得最大价值
转移 如果\((i>>r)and1==0其中0<=r<N]\)那么\(F[i|(1<<r)][r][k+v_r]=chkmax(F[i][j][k]+w_jxorw_r)\)
2.吃奶酪
这类问题叫TSP问题。
第一维压缩当前哪些点走过了,第二维记录停在哪里
转移枚举下一次去哪即可
3.售货员的难题
同上
4.[USACO]Corn Fields G
\(F[i][j]\)表示当前考虑到第\(i\)行,上一行状态压缩完为\(j\)
转移考虑枚举\(j\)的补集的子集
复杂度是\(O(3^n)\) 一般适用于\(14<=n<=16\)
5.互不侵犯
6.中国象棋 - 摆上马
卡空间毒瘤题。
8.博弈DP
zu。
二人博弈
组合博弈
sg定理。
一般DP
无迹可寻。
1.[AHOI2009]中国象棋
设\(F[i][j][k][l]\)表示当前放完了前\(i\)行,当前有\(0/1/2\)个炮的列数是\(j/k/l\)的方案数是多少
发现后三维相加等于总列数,所以可以压掉一维
于是变为\(F[i][j][k]\)
转移就是考虑当前这一行放\(0/1/2\)个炮,每个炮放在了有几个炮的一列上
时间复杂度\(O(nm^2)\) 代码
2.*关路灯
3.HDU 5009
好题。