动态规划的常见模型与技巧,涵盖线性 DP、区间 DP、树形 DP、状态压缩
DP、数位 DP 等典型类型,重点介绍最长公共子序列、最长上升子序列、01
背包及其扩展(完全背包、多重背包、混合背包),并深入讲解状态设计、转移方程、边界初始化、滚动数组优化及单调队列优化等核心技巧。
线性DP
理解题目递推的顺序
守望者的逃离 摆花 线段 乌龟棋 最长公共子序列
1 2 3 4 5 6 7 8 9 10 s1 = io[0 ]; s2 = io[1 ]; n = s1.length(); m = s2.length(); dp = new int [n+1 ][m+1 ]; for (int i = 1 ;i <= n;i++){ for (int j = 1 ;j <= m;j++){ if (s1.charAt(i-1 ) == s2.charAt(j-1 )) dp[i][j] = dp[i-1 ][j-1 ]+1 ; else dp[i][j] = max(dp[i-1 ][j], dp[i][j-1 ]); } } out.println(dp[n][m]);
P1439
【模板】最长上升子序列
把题目转换一下 两个序列其中一个转换为1 2 … n 排列 置换另一个
计算最长上升子序列即可
O (n 2 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static int [] a = new int [N], dp = new int [N]; static void solve () throws Exception {n = ni(); for (int i = 1 ;i <= n;i++) a[i] = ni();for (int i = 1 ;i <= n;i++){ dp[i] = 1 ; for (int j = i-1 ;j >= 1 ;j--){ if (a[i] > a[j]) dp[i] = max(dp[i], dp[j]+1 ); } } int ans = 0 ; for (int i = 1 ;i <= n;i++) ans = max(ans, dp[i]);out.println(ans); }
优化:原来的d p 数组的存储由数值换成该序列中,上升子序列长度为i的上升子序列,的最小末尾数值
这其实就是一种近贪心的思想:我们当前的上升子序列长度如果已经确定,那么如果这种长度的子序列的结尾元素越小,后面的元素就可以有更大概率地加入到这条上升子序列中。
O (n log n )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static int [] a = new int [N], dp = new int [N];static void solve () throws Exception { n = ni(); for (int i = 1 ;i <= n;i++) a[i] = ni(); Arrays.fill(dp, 0 , n, 1 <<30 ); int len = 0 ; for (int i = 1 ;i <= n;i++){ int cur = a[i]; int l = 0 , r = len-1 ; while (l <= r){ int mid = l+r >> 1 ; if (dp[mid] < cur) l = mid+1 ; else r = mid-1 ; } if (l == len) ++len; dp[l] = min(dp[l], cur); } out.println(len); }
背包DP
01背包
1 2 3 4 5 6 7 8 9 static int [][] dp = new int [N][M];static int [] w = new int [N], v = new int [N]; for (int i = 1 ;i <= n;i++){ for (int j = 0 ;j <= m;j++){ if (j >= w[i]) dp[i][j] = max(dp[i-1 ][j-w[i]]+v[i], dp[i-1 ][j]); else dp[i][j] = dp[i-1 ][j]; } } out.println(dp[n][m]);
1 2 3 4 5 6 7 8 9 10 static int [] dp = new int [M];static int [] w = new int [N], v = new int [N]; for (int i = 1 ;i <= n;i++){ for (int j = m;j >= 0 ;j--){ if (j >= w[i]) dp[j] = max(dp[j-w[i]]+v[i], dp[j]); } } out.println(dp[m]);
P1048 采药 P1049 装箱问题
P1164 小A点菜
计数问题:装满背包的方案数
1 2 3 4 5 6 7 8 9 static int [] dp = new int [M];static int [] w = new int [N]; dp[0 ] = 1 ; for (int i = 1 ;i <= n;i++){ for (int j = m;j >= 0 ;j--){ if (j >= w[i]) dp[j] += dp[j-w[i]]; } } out.println(dp[m]);
P1507
NASA的食物计划 二维01背包(就又加了个限制条件而已)
1 2 3 4 5 6 7 8 9 10 int [][] dp = new int [M][M]; int [] w1 = new int [N], w2 = new int [N], v = new int [N]; for (int i = 1 ;i <= n;i++){ for (int j = m;j >= w1[i];j--){ for (int k = t;k >= w2[i];k--) { dp[j][k] = max(dp[j-w1[i]][k-w2[i]]+v[i], dp[j][k]); } } } out.println(dp[m][t]);
完全背包
1 2 3 4 5 6 7 8 static int [] dp = new int [M];static int [] w = new int [N], v = new int [N];for (int i = 1 ;i <= n;i++){for (int k = 0 ;k*w[i] <= m;k++){ for (int j = m;j >= k*w[i];--j) dp[j] = max(dp[j], dp[j-k*w[i]]+k*v[i]); } }
1 2 3 4 5 6 for (int i = 1 ;i <= n;i++){ for (int j = w[i];j <= m;++j) dp[j] = max(dp[j], dp[j-w[i]]+v[i]); } out.println(dp[m]);
首先考虑二维:d p [i ][j ]
怎么得到(其实就是上面写的暴力)
两 式 联 立 即 可 导 出
考虑压缩到一维:
d p [i ][j ] 更新要用到d p [i − 1][j ], d p [i ][j − v ]
即第i 层的j 要用到第i 层的j − v 来更新
所以==第二层遍历顺序应该从小到大(与01背包的区别)==
USACO08NOV]Buying
Hay S P1853
投资的最大效益
多重背包
1 2 3 4 5 6 7 8 for (int i = 1 ;i <= n;i++) { for (int j = m;j >= v[i];j--) { for (int k = 1 ;k <= s[i] && j>=k*v[i];k++){ dp[j] = max(dp[j], dp[j-k*v[i]]+k*w[i]); } } } out.println(dp[m]);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int [] dp = new int [M];static int [] w = new int [N], v = new int [N]; int cnt = 1 ;for (int i = 1 ;i <= n;i++){ int wi = ni(), vi = ni(), s = ni(); for (int c = 1 ;c <= s;c <<= 1 ){ w[cnt] = c*wi; v[cnt++] = c*vi; s -= c; } if (s != 0 ) {w[cnt] = s*wi; v[cnt++] = s*vi;} } for (int i = 1 ;i < cnt;i++){ for (int j = m;j >= w[i];--j) dp[j] = max(dp[j], dp[j-w[i]]+v[i]); } out.println(dp[m]);
混合背包
物品分三类:
第一类物品只能用 1 次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 s 次(多重背包);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static int [] dp = new int [M];static int [] w = new int [N], v = new int [N], kind = new int [N]; int n, m, cnt = 1 ;for (int i = 1 ;i <= n;i++){ int wi = ni(), vi = ni(), s = ni(); if (s == 0 ){ w[cnt] = wi; v[cnt] = vi; kind[cnt++] = 1 ; }else { for (int k = 1 ;k <= s;k <<= 1 ){ w[cnt] = wi*k; v[cnt] = vi*k; kind[cnt++] = 0 ; s -= k; } if (s != 0 ) {w[cnt] = wi*s; v[cnt] = vi*s; kind[cnt++] = 0 ;} } } for (int i = 1 ;i < cnt;i++){ if (kind[i] == 0 ){ for (int j = m;j >= w[i];--j) dp[j] = max(dp[j], dp[j-w[i]]+v[i]); }else { for (int j = w[i];j <= m;++j) dp[j] = max(dp[j], dp[j-w[i]]+v[i]); } } out.println(dp[m]);
分组背包
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 vij
,价值是 wij ,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
常出现在树形DP中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { int mod = (int )1e9 +7 ; public int numRollsToTarget (int n, int m, int t) { int [][] f = new int [n + 1 ][t + 1 ]; f[0 ][0 ] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= t; j++) { for (int k = 1 ; k <= m; k++) { if (j >= k) { f[i][j] = (f[i][j] + f[i-1 ][j-k]) % mod; } } } } return f[n][t]; } }
区间DP
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由
前一阶段的哪些元素合并而来有很大的关系。令状态 f (i , j )
表示将下标位置 i 到 j 的所有元素合
并能获得的价值的最大值,那么 f (i , j ) = max {f (i , k ) + f (k + 1, j ) + c o s t },c o s t
为将这 两组元素合并起来的代价。
注意:f (i , j ) 由 f (i , k ) 和 f (k + 1, j )
得到 所以 直接两层循环i 和 j 是不行的
比如 i循环到1 j循环到5 需要【1, 3】【4, 5】此时【4,
5】还没有求出来(因为i才循环到1)
所以我们要枚举区间长度 从小到大
P1063
能量项链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for (int i = 1 ;i <= n;i++) { a[i-1 ][1 ] = a[i][0 ] = nl(); a[i-1 +n] = a[i-1 ]; } a[n][1 ] = a[1 ][0 ]; long res = 0 ;for (int len = 2 ;len <= n;len++){ for (int i = 1 ;i+len-1 < 2 *n;i++){ int j = i+len-1 ; for (int k = i;k < j;k++){ dp[i][j] = max(dp[i][j], dp[i][k]+dp[k+1 ][j]+ a[i][0 ]*a[k+1 ][0 ]*a[j][1 ]); if (dp[i][j] > res) res = dp[i][j]; } } } out.println(res);
状态压缩DP
Cows in a Skyscraper
G
给出n个物品,体积为w[i],现把其分成若干组,要求每组总体积<=k,问最小分组。(n<=18)
我们用1代表这个奶牛已经在电梯里了,0则还没坐电梯
那么我们可以直接定义一维的数组,f[i]就代表当前状态为i时最小的乘电梯次数
最终的答案就是f[(1<<n)-1];
我们可以开一个数组g来记录当前状态下电梯内剩余的体积
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void solve () throws Exception {n = ni(); k = ni(); for (int i = 0 ;i < n;i++) a[i] = ni();Arrays.fill(dp, 1 <<30 ); dp[0 ] = 1 ; g[0 ] = k; for (int i = 0 ;i < (1 <<n);i++){ for (int j = 0 ;j < n;j++){ if ((i>>j & 1 ) == 1 ) continue ; if (g[i] >= a[j]){ dp[i | (1 <<j)] = dp[i]; g[i | (1 <<j)] = max(g[i | (1 <<j)], g[i]-a[j]); }else if (dp[i | (1 <<j)] >= dp[i]+1 ) { dp[i | (1 <<j)] = dp[i]+1 ; g[i | (1 <<j)] = max(g[i | (1 <<j)], k-a[j]); } } } out.println(dp[(1 <<n)-1 ]); }
数位DP
数位
DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:
要求统计满足一定条件的数的数量(即,最终目的为计数);
这些条件经过转化后可以使用「数位」的思想去理解和判断;
输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
上界很大,暴力枚举验证会超时。
一般的数位dp中,任何为0的数位都对结果都不影响;但对于某些题目,最高位为0时,对结果不影响,但非最高位为0时会对答案有贡献;此时需要添加前导零标记,以用于判断当前位置是否为最高位。
windy 数
不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy
想知道,在 a 和 b 之间,包括 a和 b,总共有多少个 windy 数?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static int [] a = new int [N];static int [][] d = new int [N][N];static int dfs (int len, int pre, boolean f, boolean z) { if (len == 0 ) return 1 ; if (!f && !z && d[len][pre] != -1 ) return d[len][pre]; int res = 0 , max = f?a[len]:9 ; for (int i = 0 ;i <= max;i++){ if (abs(i-pre) < 2 && !z) continue ; res += dfs(len-1 , i, f&&i==a[len], z&&i==0 ); } if (!f && !z) d[len][pre] = res; return res; } static int get (int x) { n = 0 ; while (x > 0 ){ a[++n] = x%10 ; x /= 10 ; } for (int i = 0 ;i < 11 ;i++) Arrays.fill(d[i], -1 ); return dfs(n, 11 , true , true ); } static void solve () throws Exception { int l = ni(), r = ni(); out.println(get(r)-get(l-1 )); }
树形DP
树形 d p
的主要实现形式是 d f s ,在 d f s 中 d p ,主要的实现形式是 d p [i ][j ][2]
基本的 d p 方程
选择节点类
树形背包类
P1352
没有上司的舞会
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r。
但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
1 2 3 4 5 6 7 8 9 10 11 static int [] a = new int [N], dp[] = new int [N][2 ];static List<Integer>[] g = new List [N];static void dfs (int u) { dp[u][0 ] = a[u]; for (int v : g[u]) { dfs(v); dp[u][0 ] += dp[v][1 ]; dp[u][1 ] += max(dp[v][1 ], dp[v][0 ]); } }
P2016
战略游戏
1 2 3 4 5 6 7 8 9 10 static void dfs (int u, int p) {dp[u][0 ] = 0 ; dp[u][1 ] = 1 ; for (int v : g[u]){ if (v == p) continue ; dfs(v, u); dp[u][0 ] += dp[v][1 ]; dp[u][1 ] += min(dp[v][1 ], dp[v][0 ]); } }