基本原理
三个条件:
- 最优子结构:能将大问题分解成小问题,并且大问题的最优解能通过小问题的最优解构成。
- 无后效性:已经求解的子问题,不会再受到后续决策的影响。
- 子问题重叠:可以用数组存下重叠的子问题来提升效率。
基本思路:
- 将原问题分解成若干个阶段,找出每个阶段对应的子问题的特征(称为状态)
- 找每个状态可能的决策,即各状态之间的转移方式(称为状态转移方程)
- 找方程的边界,确定转移的顺序。
(可以按顺序递推着写,不好确定顺序时也可以记忆化搜索)
本质上可以对应到图上,状态为节点,决策为边,构成DAG,按拓扑序递推。
线性DP
LIS & LCS
LIS(Longest Increasing Subsequence 最长上升子序列)
计算序列\(A\)的LIS
Sol:
定义\(f_i\)表示考虑到前\(i\)个数,以第\(i\)个数结尾的LIS长度(状态)。
\(f_i=\max\limits_{j<i \land a_j<a_i} f_j+1\)(状态转移方程)
\(f_1=1\)(边界)
从前往后转移即可(顺序)。
最后\(ans=\max\limits_{i=1}^n f_i\)
\(O(n^2)\)
Another Sol:
上面的算法太慢了,考虑优化。
记\(f_i\)表示所有长度为\(i\)的LIS最后一项的最小值,特别地,若长度为\(i\)的LIS不存在,则\(f_i=INF\)
下面证明:随着\(i\)增大,\(f_i\)单调递增。
运用反证法。假设存在\(x<i\),使得\(f_x>f_i\),那么在以\(f_i\)结尾的长度为\(i\)的LIS中可以分离出以\(f_i\)结尾的长度为\(x\)的LIS,这与\(f_x\)的最小性矛盾。所以命题得证。
现在来考虑以\(i\)结尾的LIS的长度\(dp_i\),由于我们要计算所有满足\(a_j<a_i\)的\(j\)中\(dp_j\)的最大值,不妨设最大的\(dp_j=x\)。
由于\(a_i>a_j \land f_x \leq a_j\),故总有\(a_i>f_x\)
又由于\(f_i\)单调递增,因此我们要找到最大的\(x\)满足\(f_x<a_i\),考虑二分。
二分出\(x\)后,\(dp_i \gets x+1\)即可。还要更新\(f\),我们只需更新\(f_{dp_i}\),这是因为\(f_1,f_2 \dots f_{dp_i-1}\)都小于\(a_i\),\(f_{dp_i}\)是第一个大于等于\(a_i\)的。
\(O(n\log n)\)
其他类似问题,最长下降子序列,最长不升子序列,最长不降子序列,考虑的方式是类似的。所以记住LIS的推导即可。
LCS(Longest Common Subsequence 最长公共子序列)
求序列\(A\)和序列\(B\)的LCS
Sol:
定义\(f_{i,j}\)表示考虑序列\(A\)中的前\(i\)个以及序列\(B\)中的前\(j\)个时LCS的长度。
考虑转移:
前两项是继承,第三项是尝试拓展。
边界:\(f_{0,0}=0\)
从前往后转移即可。
\(O(n^2)\)
Another Sol:
上面的算法太慢了,考虑优化。
我们把序列\(A\)离散化成一个排列,序列\(B\)与之对应。
再将排列中的每个数对应到它的下标,则序列\(A\)变成一个从\(1\)到\(n\)的序列,序列\(B\)与之对应。
此时LCS长度不变(这由映射关系是显然的),并且注意到LCS的长度等于序列\(B\)的LIS的长度。
于是转到对LIS的优化。
(注意序列\(B\)中的元素要都在序列\(A\)中出现过才能使用这种方法)
\(O(n\log n)\)
背包
01背包
\(n\)个物品,背包容量为\(m\),每个物品有体积\(v_i\)(有时也叫重量或者其他类似概念)和价值\(w_i\),每个物品只能取\(1\)次,求在物品总体积不超过背包容量的情况下能得到的最大价值。
Sol:
定义\(f_{i,j}\)表示考虑了前\(i\)个物品,背包容量为\(j\)时可以得到的最大价值。
\(f_{i,j}=\max(f_{i-1,j},f_{i-1,j-v_i}+w_i)\)
前一项是继承,后一项是尝试做选择第\(i\)个物品的决策。
\(f_{0,0}=0\)
从前往后转移即可。
\(O(nm)\)
代码实现可以压掉第一维,但注意转移顺序,要倒着转移(显然的,看转移方程)。
回退背包
自己起的名字。可能不叫这个。
01背包算好后其中的物品是无序的,可以随便钦定一个物品作为最后一个加入的,然后倒着跑背包撤销这个物品的影响。
注意这个东西只能在信息支持简单撤销时能用,比如加法改成减法,求\(\max\)之类的就不行了。
P4141 消失之物
完全背包
与01背包类似,但一个物品可以取无限次。
有两种解决方式:
The First Sol:
把每种物品拆成每\(2^k\)个一组(二进制分组),显然分的组是有限个,对这些组跑01背包。
The Second Sol:
同01背包定义\(f_{i,j}\)
注意到与01背包的区别只在于做决策时的下标。
第一维同样可以压掉,但是注意转移顺序,正着转移即可。
\(O(nm)\)
多重背包
与01背包类似,但第\(i\)个物品有\(s_i\)个。
Sol:
考虑二进制分组,每\(2^k\)个分一组,最后不足的单独分一组。
可以证明这样可以组合出每一种决策。
然后转化为01背包做。
\(O(nm\log s)\)
混合背包
01背包、完全背包、多重背包混在一起。
Sol:
分类讨论一下,把三种代码拼在一起。
或者都用二进制分组。
二维(多维)费用背包
在背包物品要消耗体积外还有其他费用(比如重量、时间等)。
Sol:
多开几维,同样可以把枚举第\(i\)个物品的第一维压掉。
分组背包
与01背包类似,但是每组内的物品只能选一个。
Sol:
每组之内做一次01背包。
具体而言,定义\(f_{i,j}\)表示考虑到第\(i\)组,背包容量为\(j\)时能获得的最大价值。
每一组内枚举其中的物品尝试转移。
更一般的线性DP
难点在于消除可能的后效性。
在线性DP中我们通过观察题目性质和优化状态设计来做到这一点。
有时会设计出形如\(f_{i,s}\)的状态表示考虑了前\(i\)个,最后一组的状态为\(s\)时的最优解,一般可以把\(s\)这一维优化掉。
区间DP
区间DP其实是特殊的线性DP,以区间长度为阶段。
基本思想是从小区间转移到大区间。
一般很板,就是从小到大枚举区间,然后枚举断点转移。
状态一般设为\(f_{l,r}\)表示\([l,r]\)这段区间的最优解。
有二维的形式,也类似,状态设为\(f_{x_1,y_1,x_2,y_2}\)表示左上角为\((x_1,y_1)\),右下角为\((x_2,y_2)\)的矩形的最优解,枚举断点改为枚举断开的行或列。
处理环
一般断环成链,将原数组复制一份接在后面即可。
转移涉及当前区间最值
可以看做是在笛卡尔树的结构上进行DP。
状压DP
本质是将一种状态压缩为一个数进行DP,除此之外与其他DP类似。
注意可以预处理出所有可行状态以提高运行效率。有时这样复杂度才正确。
高级的状压:插头DP。
树形DP
一般的树形DP
只是将DP的操作放到树上,利用树很好的递归性质求解。
一般定义状态为\(f_{u,s}\)表示在\(u\)的子树内限制为\(s\)的最优解。
尝试从\(son/fa\)转移。
树上背包
就是把背包放到树上。
定义状态为\(f_{u,i,j}\)表示在\(u\)的子树内,考虑了前\(i\)个儿子,背包容量为\(j\)时能获得的最大价值。
转移:\(f_{u,i,j}=\max\limits_{v \in son_u,k \le j\land k\le siz_v} f_{u,i-1,j-k}+f_{v,siz_v,k}\)
注意边界,手推一下。
第二维可以压掉,但要倒序枚举\(j\)(显然的,看转移方程)。
树上换根
也叫二次扫描,通常要两次DFS。第一次进行一些预处理,第二次开始DP
转移时可以考虑从父亲转移到儿子(因为一般以\(1\)为根时的答案可以在第一次DFS后简单地算出来,然后向下转移)。
关注换根时答案的变化量。
数位DP
问题特征
-
目的是统计满足某种限制的数的个数。
-
限制是针对数位的。
-
提供统计的区间或上界。
-
上界很大,但只看数位个数可以接受。
基本原理
有一个通用答案数组,其中记录在计数过程中大量重复出现的过程的答案(如统计\(1000\)到\(1999\)的答案和\(2000\)到\(2999\)的答案,后三位都是\(000\)到\(999\),可以单独记录下来)。
这个通用答案数组根据题目限制进行DP
区间的询问通常拆成两个区间相减。
统计答案部分可以记忆化搜索,也可以递推。从高到低枚举每一位,注意贴上限时的处理(如上限为\(12345\),现在填的前三位为\(123\))。
部分套路:
第\(i\)位上能填的数与前面填的数相关。
定义状态为\(f_{i,s,op}\)表示当前考虑到第\(i\)位,前缀的状态为\(s\),第\(i\)位与前缀的关系为\(op\)。
根据题意尝试转移。
还可以尝试继续压缩常数,参数里面能相互推的压在一起。
记忆化搜索
记搜大法吼!
记搜长得很板,方便拓展。
一般先将边界数字拆到数位上,然后从高位到低位填数。用个数组记忆化一下。
一般有以下形参:
-
\(pos\):当前枚举到的位置。
-
\(lim\):第\(pos\)位是否受限(前几位是否贴上界)。初始时可以理解为前面都贴了\(0\)的上界。
-
\(lead\):第\(pos\)位前面是否有前导零。前导零与答案有关时才记,零只有在不是前导零时才算贡献。
接着是根据题意得到的限制条件,记搜时要作为状态的一部分塞到形参里以及状态里。
考虑用于记忆化的数组\(f\),\(f_{i,op}\)表示当前\([len,pos+1]\)都填好了,且满足限制\(op\),\([1,pos]\)随便填(这就要求!lim&&!lead
(没有限制)才能返回\(f\))的答案。
计数DP
特征:计算满足限制的方案总数。
注意计数时要求的不重不漏。有重复时考虑容斥减掉或者设计一种DP顺序使方案不重复。
一般是数学推式子再套上DP
概率期望DP
就是概率期望相关的数学推式子套上DP
一些套路:
对于概率DP,一般是正着DP,即定义状态\(f_i\)表示从初状态到状态\(i\)的概率。
对于期望DP,一般是倒着DP,即定义状态\(f_i\)表示从状态\(i\)到末状态的期望。(有些题目也可以正着DP,但是有些麻烦)
插头DP
连通性相关的状压DP。
适用于各种网格覆盖相关且允许状压的问题。
SOS DP
Sum Over Subset DP,子集和DP。是特殊情况的高维前缀和。
每一维只是\(0\)/\(1\)的情况比较常见,比如二进制数视作集合,然后求\(a_i\)中子集的和。
表述更为清晰一点的例子是,求\(a_{1\dots n}\)中有多少个\(i\)满足\(a_i \And k=k\)
复杂度与值域有关,设值域为\(V\),则复杂度为\(O(V\log V)\)。
现在来讲做法,设\(f_{i,j}\)表示前\(i\)位与\(j\)可以不同,其他位必须相同的子集的和。
那么就有分讨了:
-
\(j\)的第\(i\)位为\(0\),那么必须选\(0\),\(f_{i,j}\)加上\(f_{i-1,j}\)。
-
\(j\)的第\(i\)位为\(1\),则\(f_{i,j}\)加上\(f_{i-1,j}+f_{i-1,j\oplus 2^i}\)
这样就不重不漏地数完了。
超集和DP也是类似的,分讨一下就做完了。
和数位DP有一定相似性,但侧重点不同。数位DP擅长处理一段连续区间中的数数问题,而SOS DP擅长处理不那么连续,而是较为离散的点的数数问题,而且点的权值可以任意给,缺点是复杂度太高。
有后效性的DP的处理方法
一般是按某个值排序(如DP值,而且不是真的排序,只是规定了顺序)。
然后可以按照类似跑最短路的方式转移。