AcWing算法基础课动态规划(Dynamic Programming, DP)模板题笔记
分析流程:
常用算法代码模板 (1) :基础算法
常用算法代码模板 (2) :数据结构
常用算法代码模板 (3) :搜索与图论
常用算法代码模板 (4) :数学知识
算法选择——由数据范围反推算法时间复杂度
有 N N N 个物品和容量为 V V V 的背包,第 i i i 个物品的体积为 v i v_i vi 、价值为 w i w_i wi ,问求在背包装得下的情况下所能挑出物品的总价值。
特点:每件物品只有一个,最多装一次
AcWing 2. 01背包问题
状态表示: f ( i , j ) f(i,j) f(i,j)
- 集合:从前 i i i 个物品中选总体积不超过 j j j 的所有选法的价值的最大值
- 属性:最大值
把 f ( i , j ) f(i,j) f(i,j) 表示的选法分为两大类——不含第 i i i 个: f ( i − 1 , j ) f(i-1,j) f(i−1,j) ;包含第 i i i 个: f ( i − 1 , j − v i ) + w i f(i-1,j-v_i)+w_i f(i−1,j−vi)+wi 。状态转移方程为
f ( i , j ) = max ( f ( i − 1 , j ) , f ( i − 1 , j − v i ) + w i ) , v i ≤ j f(i,j)=\max(f(i-1,j),\ f(i-1,j-v_i)+w_i),\ v_i≤j f(i,j)=max(f(i−1,j), f(i−1,j−vi)+wi), vi≤j
/* 暴力DP */ for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) { f[i][j] = f[i - 1][j]; if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); }
观察代码可知01背包的当前状态( i i i )仅与前一状态( i − 1 i-1 i−1 )有关,故可使用滚动数组,将空间缩小到一维 f ( j ) f(j) f(j) 。改写时需注意:转移时直接使用上一层( i − 1 i-1 i−1)的体积时应从大到小枚举,防止覆盖;使用本层( i i i)的体积则从小到大枚举!
本题应当将 j j j 改为从大到小枚举,优化后的完整题解如下。
时间复杂度: O ( n 2 ) O(n^2) O(n2)
#include #include #include using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int f[N]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]); for (int i = 1; i <= n; i++) for (int j = m; j >= v[i]; j++) // 根据状态转移方程,应从大到小枚举 f[j] = max(f[j], f[j - v[i]] + w[i]); printf("%d\n", f[n][m]); return 0; }
特点:每件物品有无限个,可无限装
AcWing 3. 完全背包问题
状态表示: f ( i , j ) f(i,j) f(i,j)
- 集合:从前 i i i 个物品中选总体积不超过 j j j 的所有选法
- 属性:价值的最大值
把 f ( i , j ) f(i,j) f(i,j) 表示的选法分成若干类,主要为两大类——不选择物品 i i i: f ( i − 1 , j ) f(i-1,j) f(i−1,j);物品 i i i 选 k ( k > 0 ) k\ (k>0) k (k>0) 个: f ( i − 1 , j − k v i ) + k w i f(i-1,j-kv_i)+kw_i f(i−1,j−kvi)+kwi 。合并这两大类情况后的状态转移方程为
f ( i , j ) = max ( f ( i − 1 , j − k v i ) + k w i ) , k ≥ 0 , k v i ≤ j f(i,j)=\max(f(i-1,j-kv_i)+kw_i),\ k≥0,\ kv_i≤j f(i,j)=max(f(i−1,j−kvi)+kwi), k≥0, kvi≤j
时间复杂度: O ( n 3 ) O(n^3) O(n3)
/* 暴力DP*/ for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) for (int k = 0; k * v[i] <= j; k++) // 合并了不选的情况(k=0) f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
优化比较次数:由状态转移方程可得 f ( i , j ) = max ( f ( i − 1 , j ) , f ( i − 1 , j − v i ) + w i , f ( i − 1 , j − 2 v i ) + 2 w i , ⋯ ) ⇒ f ( i , j − v i ) = max ( f ( i − 1 , j − v i ) , f ( i − 1 , j − 2 v i ) + w i , ⋯ ) f(i,j)=\max(f(i-1,j),\ f(i-1,j-v_i)+w_i,\ f(i-1,j-2v_i)+2w_i,\ \cdots) \Rightarrow \\f(i,j-v_i)=\max(f(i-1,j-v_i),\ f(i-1,j-2v_i)+w_i,\ \cdots) f(i,j)=max(f(i−1,j), f(i−1,j−vi)+wi, f(i−1,j−2vi)+2wi, ⋯)⇒f(i,j−vi)=max(f(i−1,j−vi), f(i−1,j−2vi)+wi, ⋯) 观察易得可用 f ( i , j − v i ) + w i f(i,j-v_i)+w_i f(i,j−vi)+wi 直接替换原方程右边最值函数中 k > 0 k>0 k>0 时的所有比较项。由此将比较时间消耗从 O ( n ) O(n) O(n) 大幅优化至 O ( 1 ) O(1) O(1),原状态转移方程可化简为
f ( i , j ) = max ( f ( i − 1 , j ) , f ( i , j − v i ) + w i ) , v i ≤ j f(i,j)=\max(f(i-1,j),\ f(i,j-v_i)+w_i),\ v_i≤j f(i,j)=max(f(i−1,j), f(i,j−vi)+wi), vi≤j
时间复杂度: O ( n 2 ) O(n^2) O(n2)
/* 优化比较次数后的DP */ for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) { f[i][j] = f[i - 1][j]; if (j >= v[i]) // 只需比较1次 f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]); }
参照01背包问题,可继续优化至一维 f ( j ) f(j) f(j) , j j j 应保持从小到大枚举(因为继续使用本层的体积/物品 i i i 可能会用到多次)。
最终优化后的完整题解如下。
#include #include #include using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int f[N]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]); for (int i = 1; i <= n; i++) for (int j = v[i]; j <= m; j++) // 根据状态转移方程,应从小到大枚举 f[j] = max(f[j], f[j - v[i]] + w[i]); printf("%d", f[m]); return 0; }
特点:每个物品 i i i有 s i s_i si 个
AcWing 4. 多重背包问题
状态表示: f ( i , j ) f(i,j) f(i,j)
- 集合:从前 i i i 个物品中选总体积不超过 j j j 的所有选法
- 属性:价值的最大值
同完全背包问题,可将 f ( i , j ) f(i,j) f(i,j) 表示的选法分成若干类:分别为选 0 , 1 , 2 , ⋯ , s i 0,1,2,\cdots,s_i 0,1,2,⋯,si 个。状态转移方程为
f ( i , j ) = max ( f ( i − 1 , j − k v i ) + k w i ) , 0 ≤ k ≤ s i , k v i ≤ j f(i,j)=\max(f(i-1,j-kv_i)+kw_i),\ 0≤k≤s_i,\ kv_i≤j f(i,j)=max(f(i−1,j−kvi)+kwi), 0≤k≤si, kvi≤j
时间复杂度: O ( n 3 ) O(n^3) O(n3) 。
#include #include #include using namespace std; const int N = 110; int n, m; int v[N], w[N], s[N]; int f[N][N]; int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d%d%d", &v[i], &w[i], &s[i]); /* 暴力DP */ for (int i = 1; i <= n; i++) for (int j = 0; j <= m; j++) for (int k = 0; k <= s[i] && k * v[i] <= j; k++) // 仅比完全背包多了数量判断 f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]); printf("%d\n", f[n][m]); return 0; }
AcWing 5. 多重背包问题 II
多重背包中每个物品为有限项,无法像完全背包一样直接替换优化。需使用二进制优化方法——将物品按组打包:对于物品 i i i 的个数 s i s_i si ,将其分解为 2 0 , 2 1 , 2 2 , ⋯ , 2 k , c 2^0,2^1,2^2,\cdots,2^k,c 20,21,22,⋯,2k,c ,其中 k = ⌊ log s i ⌋ , c = s i − ( 2 0 + 2 1 + ⋯ + 2 k ) k=\lfloor \log s_i\rfloor,\ c=s_i-(2^0+2^1+\cdots+2^k) k=⌊logsi⌋, c=si−(20+21+⋯+2k)。因为用 2 0 , 2 1 , 2 2 , ⋯ , 2 k 2^0,2^1,2^2,\cdots,2^k 20,21,22,⋯,2k 可拼出 [ 0 , 2 k + 1 − 1 ] [0,\ 2^{k+1}-1] [0, 2k+1−1] 上所有整数,又 c = s i − ( 2 k + 1 − 1 ) < 2 k + 1 c=s_i-(2^{k+1}-1)<2^{k+1} c=si−(2k+1−1)<2k+1 ,所以用 2 0 , 2 1 , 2 2 , ⋯ , 2 k , c 2^0,2^1,2^2,\cdots,2^k,c 20,21,22,⋯,2k,c 可拼出 [ 0 , s i ] [0,\ s_i] [0, si] 上所有整数。
由此枚举部分的时间消耗从 O ( n ) O(n) O(n) 降至 O ( log n ) O(\log n) O(logn) 。同时将问题转化为01背包问题(每一组有且只有选或不选两种方案),可以再进一步优化成一维求解。
时间复杂度: O ( n 2 log n ) O(n^2\log n) O(n2logn)
#include #include #include using namespace std; const int N = 12010, M = 2010; // N = 1000 * log2(1000) int n, m; int v[N], w[N]; int f[M]; // 降为一维,只需开1e3量级的长度 int main() { scanf("%d%d", &n, &m); int cnt = 0; // 记录打包分组后的物品的个数 for (int i = 1; i <= n; i++) { int tv, tw, s; scanf("%d%d%d", &tv, &tw, &s); int k = 1; // 2的cnt次幂 while (k <= s) { // 每k个为一组 cnt++; v[cnt] = tv * k; w[cnt] = tw * k; s -= k; k *= 2; } if (s > 0) { // 剩余的单独一组 cnt++; v[cnt] = tv * s; w[cnt] = tw * s; } } /* 二进制优化后,将多重背包转化成01背包 */ n = cnt; // 更新为打包后物品的数量(每种物品只有1个) for (int i = 1; i <= n; i++) for (int j = m; j >= v[i]; j--) f[j] = max(f[j], f[j - v[i]] + w[i]); printf("%d\n", f[m]); return 0; }
特点:所有物品分若干组,每组里只能选一个
AcWing 9. 分组背包问题
状态表示: f ( i , j ) f(i,j) f(i,j)
- 集合:从前 i i i组物品中选总体积不超过 j j j 的所有选法
- 属性:价值的最大值
可将 f ( i , j ) f(i,j) f(i,j) 表示的选法分成若干类——第 i i i 组不选: f ( i − 1 , j ) f(i-1,j) f(i−1,j);第 i i i 组的 s i s_i si 个物品中选第 k ( k > 0 ) k\ (k>0) k (k>0) 个物品: f ( i − 1 , j − v i k ) + w i k f(i-1,j-v_{ik})+w_{ik} f(i−1,j−vik)+wik 。状态转移方程为 f ( i , j ) = max ( f ( i − 1 , j ) , f ( i − 1 , j − v i k ) + w i k ) , 0 < k ≤ s i , v i k ≤ j f(i,j)=\max(f(i-1,j),\ f(i-1,j-v_{ik})+w_{ik}),\ 0 状态转移方程存在明显线性关系。 AcWing 898. 数字三角形 状态表示: f ( i , j ) f(i,j) f(i,j) f ( i , j ) f(i,j) f(i,j) 表示的集合可分为两类——来自左上: f ( i − 1 , j − 1 ) + a i j f(i-1,j-1)+a_{ij} f(i−1,j−1)+aij 、来自右上: f ( i − 1 , j ) + a i j f(i-1,j)+a_{ij} f(i−1,j)+aij 。状态转移方程为 时间复杂度: O ( n 2 ) O(n^2) O(n2) AcWing 895. 最长上升子序列 状态表示: f ( i ) f(i) f(i) f ( i ) f(i) f(i) 表示的集合可分为若干类(不一定都存在)——上升子序列中只有 a i a_i ai 一个数: 1 1 1 ;倒数第二个数是 a j ( 1 ≤ j ≤ i − 1 ) a_j\ (1≤j≤i-1) aj (1≤j≤i−1) : f ( j ) + 1 f(j)+1 f(j)+1(若存在 a j < a i a_j 时间复杂度: O ( n 2 ) O(n^2) O(n2) AcWing 896. 最长上升子序列 II 贪心优化 思路:如果以 a j a_j aj 结尾和以 a k a_k ak 结尾的上升子序列长度相同,那么就取 a j a_j aj 和 a k a_k ak 之中较小的那一个的子序列,小的数更有可能构成更长的上升子序列。 因此可以采用贪心思想,开一个变长数组 对于 a i a_i ai ,在 时间复杂度: O ( n 2 ) O(n^2) O(n2) 二分优化 时间复杂度: O ( n log n ) O(n\log n) O(nlogn) AcWing 897. 最长公共子序列 状态表示: f ( i , j ) f(i,j) f(i,j) 根据字母 a i , b j a_i,\ b_j ai, bj 的选择情况(分别用2位二进制数的每一位来表示, 0 0 0 : 不选, 1 1 1 : 选)来划分 f ( i , j ) f(i,j) f(i,j) 表示的集合—— 00 , 01 , 10 , 11 00,\ 01,\ 10,\ 11 00, 01, 10, 11 ,其中易得 00 00 00: f ( i − 1 , j − 1 ) f(i-1,j-1) f(i−1,j−1); 11 11 11: f ( i − 1 , j − 1 ) + 1 f(i-1,j-1)+1 f(i−1,j−1)+1 。 尽管 01 01 01 情况无法简单给出准确的状态表示,与 f ( i − 1 , j ) f(i-1,j) f(i−1,j) 所表示的集合不完全等同,但由上述集合定义可知 f ( i − 1 , j ) f(i-1,j) f(i−1,j) 包含 01 01 01 情况,因此可用其替代。同理, f ( i , j − 1 ) f(i,j-1) f(i,j−1) 包含 10 10 10 ,也可作为替代。根据 max ( A , B , C ) = max ( max ( A , B ) , max ( B , C ) ) \max(A,B,C)=\max(\max(A,B),\max(B,C)) max(A,B,C)=max(max(A,B),max(B,C))(其中 B B B 的重复出现不影响最终最值结果)可知,尽管该种集合划分方式显然存在重复问题,但由于所求的是最值,重复划分不会影响最终结果,注意如果所求的是数量则不得重复划分。 观察可知, 00 00 00 情况也包含于 f ( i − 1 , j ) f(i-1,j) f(i−1,j) 和 f ( i , j − 1 ) f(i,j-1) f(i,j−1) 之中,因此实际只有3种情况: f ( i − 1 , j ) , f ( i , j − 1 ) , f ( i − 1 , j − 1 ) + 1 f(i-1,j),\ f(i,j-1),\ f(i-1,j-1)+1 f(i−1,j), f(i,j−1), f(i−1,j−1)+1 。 状态转移方程为 AcWing 902. 最短编辑距离 状态表示: f ( i , j ) f(i,j) f(i,j) 要使得 因此状态转移方程为 f ( i , j ) = { min ( f ( i − 1 , j ) + 1 , f ( i , j − 1 ) + 1 , f ( i − 1 , j − 1 ) ) , a i = b j min ( f ( i − 1 , j ) + 1 , f ( i , j − 1 ) + 1 , f ( i − 1 , j − 1 ) + 1 ) , a i ≠ b j f(i,j)=\begin{cases} \min(f(i-1,j)+1,f(i,j-1)+1,f(i-1,j-1)), & a_i=b_j\\ \min(f(i-1,j)+1,f(i,j-1)+1,f(i-1,j-1)+1), & a_i≠b_j \end{cases} f(i,j)={min(f(i−1,j)+1,f(i,j−1)+1,f(i−1,j−1)),min(f(i−1,j)+1,f(i,j−1)+1,f(i−1,j−1)+1),ai=bjai=bj 边界处理:当其中一方为空串时,修改次数即为另一方非空串的长度,则 f ( i , 0 ) = i , f ( 0 , j ) = j f(i,0)=i,\ f(0,j)=j f(i,0)=i, f(0,j)=j 。 时间复杂度: O ( n 2 ) O(n^2) O(n2) 变式:AcWing 899. 编辑距离 核心DP方法完全一致,考察字符串的存储与处理。 AcWing 282. 石子合并 状态表示: f ( i , j ) f(i,j) f(i,j) 分治区间 [ i , j ] [i,\ j] [i, j] 为左右子区间,根据左子区间的划分情况将 f ( i , j ) f(i,j) f(i,j) 表示的集合按如下方式分成若干类—— 1 , 2 , 3 , ⋯ , j − i − 1 , j − i 1,2,3,\cdots,j-i-1,j-i 1,2,3,⋯,j−i−1,j−i 。设划分给左子区间的右端点为 k k k ,则将 [ i , k ] , [ k + 1 , j ] [i,\ k],[k+1,\ j] [i, k],[k+1, j] 合并后的代价的最小值为两侧子区间上的最小值之和再加上本区间上所有石子的质量(此为合并子区间必须进行的不可分解的最小操作),即 f ( i , k ) + f ( k + 1 , j ) + s j − s i − 1 f(i,k)+f(k+1,j)+s_j-s_{i-1} f(i,k)+f(k+1,j)+sj−si−1 ,其中 s i s_i si 表示第 i i i 个石子的质量对应的前缀和。状态转移方程为 时间复杂度: O ( n 3 ) O(n^3) O(n3) AcWing 900. 整数划分 实为要求恰好装满背包的完全背包问题。 状态表示: f ( i , j ) f(i,j) f(i,j) 直接参照完全背包问题转移的推导与优化过程即可,可以视作权值等同下标 v i = i v_i=i vi=i ,则无需取最值,状态转移方程可化简为 f ( i , j ) = f ( i − 1 , j ) + f ( i , j − i ) f(i,j)=f(i-1,j)+f(i,j-i) f(i,j)=f(i−1,j)+f(i,j−i) 同样可以继续降为一维 f ( j ) f(j) f(j) 。 状态表示: f ( i , j ) f(i,j) f(i,j) 根据所选的这 j j j 个数里的最小值将 f ( i , j ) f(i,j) f(i,j) 表示的集合划分为两部分——最小值为 1 1 1 :当前所有方案均去掉这个“ 1 1 1”,即得 f ( i − 1 , j − 1 ) f(i-1,j-1) f(i−1,j−1) ;最小值 > 1 >1 >1 :这 j j j 个数各自减 1 1 1 后仍为正整数,即得 f ( i − j , j ) f(i-j,j) f(i−j,j)。状态转移方程为 重点在逻辑上的分类讨论。 AcWing 338. 计数问题 使用前缀和的思想,将求区间上的解转化成求前缀的解:定义函数 count ( n , x ) \text{count}(n, x) count(n,x) 表示 [ 1 , n ] [1,\ n] [1, n] 上的数各数位中数字 x x x 出现的次数。根据前缀和的思想,原题的解即为 count ( b , x ) − count ( a − 1 , x ) \text{count}(b, x)-\text{count}(a-1, x) count(b,x)−count(a−1,x) ,因此将原题转化为子问题:求 [ 1 , n ] [1,\ n] [1, n] 上的数各数位中数字 x x x 出现的次数。设 n = a 1 a 2 ⋯ a m ‾ n=\overline{a_1a_2\cdots a_m} n=a1a2⋯am ,其中 m m m 为 n n n 的位数。求 x x x 在第 i i i 位上出现的次数 C i ( n , x ) C_i(n,x) Ci(n,x) 的过程如下: 由 1 ≤ b 1 b 2 ⋯ b i − 1 x b i + 1 ⋯ b m ‾ ≤ a 1 a 2 ⋯ a i − 1 a i a i + 1 ⋯ a m ‾ = n 1≤\overline{b_1b_2\cdots b_{i-1}xb_{i+1}\cdots b_m}≤\overline{a_1a_2\cdots a_{i-1}a_ia_{i+1}\cdots a_m}=n 1≤b1b2⋯bi−1xbi+1⋯bm≤a1a2⋯ai−1aiai+1⋯am=n 得 注意 x = 0 x=0 x=0 的情况: 综上, x x x 在 n n n 的第 i i i 位上出现的次数 C i ( n , x ) = { ( a 1 a 2 ⋯ a i − 1 ‾ − c ) × 1 0 m − i , a i < x ( a 1 a 2 ⋯ a i − 1 ‾ − c ) × 1 0 m − i + a i + 1 a i + 2 ⋯ a m ‾ + 1 , a i = x ( a 1 a 2 ⋯ a i − 1 ‾ − c ) × 1 0 m − i + 1 0 m − i , a i > x , c = { 0 , x ≠ 0 1 , x = 0 C_i(n,x)=\begin{cases} (\overline{a_1a_2\cdots a_{i-1}}-c)\times 10^{m-i}, & a_i 用二进制数保存状态:二进制数的每一位表示一个物品, 0 / 1 0/1 0/1 表示不同的状态。 AcWing 291. 蒙德里安的梦想 摆完横向小方格之后纵向小方格的位置是唯一确定的,因此只需考虑按照列摆放横向小方格。可以用 N N N 的二进制数( [ 0 , 2 N − 1 ] [0,\ 2^N-1] [0, 2N−1] 上的所有整数)来存储全部 N N N 行的每一行状态。 状态表示: f ( i , j ) f(i,j) f(i,j) 对于 f ( i , j ) f(i,j) f(i,j) ,设第 i − 1 i-1 i−1 列的状态表示数为 k k k ,则可进行状态转移的条件为—— 满足上述2个条件可进行转移操作—— f ( i , j ) ← f ( i , j ) + f ( i − 1 , k ) f(i,j)\leftarrow f(i,j)+f(i-1,k) f(i,j)←f(i,j)+f(i−1,k) AcWing 91. 最短Hamilton路径 压缩存储路径。 状态表示: f ( i , j ) f(i,j) f(i,j) 参考线性DP中“倒数第二个点”的分析方法,将 f ( i , j ) f(i,j) f(i,j) 表示的集合进行划分——设倒数第二个点为 k ( 0 ≤ k < n − 1 ) k\ (0≤k AcWing 285. 没有上司的舞会 状态表示: f ( u , 0 ) , f ( u , 1 ) f(u,0),\ f(u,1) f(u,0), f(u,1) 递归求解:对于 f ( u , 0 ) f(u,0) f(u,0) (选了点 u u u ,故初值为其快乐值 H u H_u Hu),加其俩儿子 s 1 , s 2 , ⋯ s_1,s_2,\cdots s1,s2,⋯ 的各状态最大值之和: f ( s 1 , 1 ) , f ( s 2 , 0 ) , ⋯ f(s_1,1),f(s_2,0),\cdots f(s1,1),f(s2,0),⋯ 或 f ( s 1 , 0 ) , f ( s 2 , 1 ) , ⋯ f(s_1,0),f(s_2,1),\cdots f(s1,0),f(s2,1),⋯ 等,;对于 f ( u , 1 ) f(u,1) f(u,1) ,则不应再选其儿子,直接加上其儿子们的 f ( s i , 0 ) f(s_i,0) f(si,0) 之和即可。由此可得状态转移方程 f ( u , 0 ) = H u + ∑ i max ( f ( s i , 0 ) , f ( s i , 1 ) ) f ( u , 1 ) = ∑ i f ( s i , 0 ) f(u,0)=H_u+\sum_i \max(f(s_i,0),f(s_i,1))\\ f(u,1)=\sum_i f(s_i,0) f(u,0)=Hu+i∑max(f(si,0),f(si,1))f(u,1)=i∑f(si,0) AcWing 901. 滑雪 状态表示: f ( i , j ) f(i,j) f(i,j) 参考搜索算法技巧,根据上、下、左、右将 f ( i , j ) f(i,j) f(i,j) 表示的集合分为4类。易知任何 f ( i , j ) f(i,j) f(i,j) 的初值均为 1 1 1 (路径仅含起点一个点),设下一步的合法(不出界、满足题设条件)坐标为 ( i ′ , j ′ ) (i',j') (i′,j′),则状态转移方程为 f ( i , j ) ← max ( f ( i , j ) , f ( i ′ , j ′ ) + 1 ) f(i,j)\leftarrow\max(f(i,j),f(i',j')+1) f(i,j)←max(f(i,j),f(i′,j′)+1)#include
2 线性DP
例1:数字三角形
f ( i , j ) = max ( f ( i − 1 , j − 1 ) + a i j , f ( i − 1 , j ) + a i j ) f(i,j)=\max(f(i-1,j-1)+a_{ij},\ f(i-1,j)+a_{ij}) f(i,j)=max(f(i−1,j−1)+aij, f(i−1,j)+aij)#include
例2:最长上升子序列
(1) 朴素算法
#include
(2) 贪心+二分优化
q[]
,长度为len
,其中q[i]
存储长度为 i i i 的上升子序列中结尾数最小的子序列的结尾数。q[]
中找到子序列长度/数组下标 k k k 尽可能大的数 q k q_k qk,满足 q k < a i q_klen
即为答案。for(int i = 0; i < n; i ++){ int k = 0; while(k < len && q[k] < a[i]) k ++; // 顺序查找子序列长度最大的小于a[i]的数 q[k] = a[i]; // a[i]无论如何都应位于位置k上 if (k == len) len ++; // 当k为len时说明查找越界,不存在小于a[i]的数,a[i]尾插 }
画图易证q[]
中元素单调递增,则可用二分来优化查找过程,查找最大的小于当前数的数(查找右边界),由上图可知 a i a_i ai 应放在该数的后一位上。最终优化后的完整题解如下。#include
例3:最长公共子序列
f ( i , j ) = { f ( i − 1 , j − 1 ) + 1 , a i = b j max ( f ( i − 1 , j ) , f ( i , j − 1 ) ) , a i ≠ b j f(i,j)=\begin{cases} f(i-1,j-1)+1, & a_i=b_j\\ \max(f(i-1,j),\ f(i,j-1)), & a_i≠b_j \end{cases} f(i,j)={f(i−1,j−1)+1,max(f(i−1,j), f(i,j−1)),ai=bjai=bj#include
例4:最短编辑距离
a[1 ... i]
转化成b[1 ... j]
所做的修改方式a[1 ... i]
与b[1 ... j]
匹配,需对a[i]
进行题中所述3种之一操作,由此反推出a[i]
、b[j]
前的字符应满足的前提条件。根据上述思路将 f ( i , j ) f(i,j) f(i,j) 表示的集合分成三类——a[i]
删除使得匹配,需先满足a[1 ... i-1]
与b[1 ... j]
匹配,集合即为 f ( i − 1 , j ) + 1 f(i-1,j)+1 f(i−1,j)+1 。a[i]
之后插入使得匹配,即执行a[i+1]=b[j]
,需先满足a[1 ... i]
与b[1 ... j-1]
匹配,集合即为 f ( i , j − 1 ) + 1 f(i, j-1)+1 f(i,j−1)+1 。a[i]
直接替换成b[j]
,需先满足a[1 ... i-1]
与b[1 ... j-1]
匹配。当a[i] != b[i]
时,集合即为 f ( i − 1 , j − 1 ) + 1 f(i-1,j-1)+1 f(i−1,j−1)+1;若已有a[i] == b[j]
,则不应执行操作,集合为 f ( i − 1 , j − 1 ) f(i-1,j-1) f(i−1,j−1) 。#include
3 区间DP
例:石子合并
f ( i , j ) = min ( f ( i , k ) + f ( k + 1 , j ) + s j − s i − 1 ) , 1 ≤ k ≤ j − i f(i,j)=\min(f(i,k)+f(k+1,j)+s_j-s_{i-1}),\ 1≤k≤j-i f(i,j)=min(f(i,k)+f(k+1,j)+sj−si−1), 1≤k≤j−i#include
4 计数类DP
例:整数划分
(1) 总和不变,个数变
#include
(2) 个数不变,总和变
f ( i , j ) = f ( i − 1 , j − 1 ) + f ( i − j , j ) f(i,j)=f(i-1,j-1)+f(i-j,j) f(i,j)=f(i−1,j−1)+f(i−j,j) 因此划分 n n n 的方案数为 f ( n , 1 ) + f ( n , 2 ) + ⋯ + f ( n , n ) f(n,1)+f(n,2)+\cdots+f(n,n) f(n,1)+f(n,2)+⋯+f(n,n)#include
5 数位统计DP
例:计数问题
#include
6 状态压缩DP
例1:蒙德里安的梦想
j & k == 0
为真。j | k
中不存在连续奇数个 0 0 0 。#include
例2:最短Hamilton路径
#include
7 树形DP
例:没有上司的舞会
#include
8 记忆化搜索
例:滑雪
#include