动态规划模板
包含了ACM一些常用的动态规划模型
思维导图:
数字三角形模型
每次只能向下走或者向右走。从起点走到终点。
题目 给定一个 n × n n \times n n × n 的矩阵,矩阵中的每个格子上有一个价值为 w w w 的物品。给定起点 ( 1 , 1 ) (1,1) ( 1 , 1 ) 和终点 ( n , n ) (n, n) ( n , n ) ,规定只能向下或者向右走。从起点出发两次,但同一个格子在两次路径中都经过的话,他的物品价值只会被累加一次。问两次路线,途径格子的物品的总价值最大是多少。
注意到每次走一步,横纵坐标的和都会加1.且知道横坐标,纵坐标,和两个坐标的和的任意两个就能知道另外一个。这就启示我们可以状态压缩。
另外两次行动可以看成一起同步行动。通过这样的想法就定义出 d p [ i ] [ j ] [ k ] dp[i][j][k] d p [ i ] [ j ] [ k ] 表示 a a a 路线当前横坐标为 i i i ,b b b 路线当前横坐标为 j j j ,横纵坐标和为 k k k 时的最大总价值。
状态只能由 d p [ i − 1 ] [ j ] [ k − 1 ] , d p [ i ] [ j − 1 ] [ k − 1 ] , d p [ i ] [ j ] [ k − 1 ] , d p [ i − 1 ] [ j − 1 ] [ k − 1 ] dp[i - 1][j][k-1],dp[i][j - 1][k- 1],dp[i][j][k - 1],dp[i-1][j-1][k-1] d p [ i − 1 ] [ j ] [ k − 1 ] , d p [ i ] [ j − 1 ] [ k − 1 ] , d p [ i ] [ j ] [ k − 1 ] , d p [ i − 1 ] [ j − 1 ] [ k − 1 ] 这四者中转移过来。如果两点不重合,再加上 a [ i ] [ k − i ] , a [ j ] [ k − 1 ] a[i][k-i],a[j][k-1] a [ i ] [ k − i ] , a [ j ] [ k − 1 ] ,否则只要加上 a [ i ] [ j − i ] a[i][j-i] a [ i ] [ j − i ] 。
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 33 34 35 36 37 38 ll n; ll a[15 ][15 ]; ll dp[15 ][15 ][30 ]; bool check (int x, int y) { if (x < 1 || x > n || y < 1 || y > n) return true ; return false ; } void solve () { n = read (); while (1 ) { int u = read (), v = read (), w = read (); if (!u && !v && !w) break ; a[u][v] = w; } dp[1 ][1 ][2 ] = 0 ; for (int k = 3 ; k <= 2 * n; k++) { for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= n; j++) { int ax = i, ay = k - i, bx = j, by = k - j; if (check (ax, ay) || check (bx, by)) continue ; ll w1 = dp[ax - 1 ][bx - 1 ][k - 1 ], w2 = dp[ax][bx - 1 ][k - 1 ]; ll w3 = dp[ax - 1 ][bx][k - 1 ], w4 = dp[ax][bx][k - 1 ]; ll w = max (max (w1, w2), max (w3, w4)); if (ax == bx) dp[i][j][k] = max (dp[i][j][k], w + a[ax][ay]); else dp[i][j][k] = max (dp[i][j][k], w + a[ax][ay] + a[bx][by]); } } } printf ("%lld\n" , dp[n][n][2 * n]); }
最长上升子序列模型
最长上升子序列:dp[i] = min{dp[j] + 1}, a[j] < a[i], j < i
最长公共子序列:dp[i] = min{dp[j] + 1}, a[j] = a[i], j < i
最长上升/下降子序列模板:
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 ll n; ll a[MAXN]; ll dpl[MAXN], dpr[MAXN]; void solve () { n = read (); for (int i = 1 ; i <= n; i++) a[i] = read (); dpl[1 ] = 1 ; for (int i = 2 ; i <= n; i++) { ll mx = 0 ; for (int j = 1 ; j < i; j++) if (a[j] < a[i]) mx = max (mx, dpl[j]); dpl[i] = mx + 1 ; } dpr[n] = 0 ; for (int i = n - 1 ; i; i--) { ll mx = -1 ; for (int j = n; j > i; j--) if (a[j] < a[i]) mx = max (mx, dpr[j]); dpr[i] = mx + 1 ; } ll ans = 0 ; for (int i = 1 ; i <= n; i++) ans = max (ans, dpl[i] + dpr[i]); printf ("%lld\n" , ans); }
经典的拦截导弹 模型。
这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
求两个量,最多能拦截的导弹数和拦截所有导弹最少要配备的系统数
第一个问题求数组的最长非上升子序列,第二个问题求该数组最少能被几个最长下降子序列覆盖。
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 33 34 ll n; ll a[MAXN]; ll dp1[MAXN]; ll dp2[MAXN]; void solve () { n = 0 ; ll tmp; while (scanf ("%lld" , &tmp) != EOF) { ++n; a[n] = tmp; } ll ans1 = 0 ; dp1[1 ] = 1 ; for (int i = 2 ; i <= n; i++) { ll mx = 0 ; for (int j = 1 ; j < i; j++) if (a[j] >= a[i]) mx = max (mx, dp1[j]); dp1[i] = mx + 1 ; } for (int i = 1 ; i <= n; i++) ans1 = max (ans1, dp1[i]); printf ("%lld\n" , ans1); dp2[1 ] = 1 ; for (int i = 2 ; i <= n; i++) { ll mx = 0 ; for (int j = 1 ; j < i; j++) if (a[j] < a[i]) mx = max (mx, dp2[j]); dp2[i] = mx + 1 ; } ll ans2 = 0 ; for (int i = 1 ; i <= n; i++) ans2 = max (ans2, dp2[i]); printf ("%lld\n" , ans2); }
最长上升公共子序列
状态表示 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] (集合): 考虑 a a a 中前 i i i 个数,b b b 中前 j j j 个数字,且当前以 b [ j ] b[j] b [ j ] 结尾的子序列方案
状态表示 d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] (属性):m a x max m a x
状态转移:
从 a a a 中前 i − 1 i - 1 i − 1 个数,b b b 中前 j j j 个数转移 d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ j ] ) dp[i][j] = max(dp[i][j],dp[i-1][j]) d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ j ] )
从 a a a 中前 i i i 个数,b b b 中前 k k k 个数且以 b [ k ] b[k] b [ k ] 为结尾的子序列方案转移过来 d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ k ] + 1 ) , k ∈ [ 1 , j − 1 ] , a i = b j dp[i][j] = max(dp[i][j],dp[i-1][k] +1),k\in[1,j-1],a_i=b_j d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i − 1 ] [ k ] + 1 ) , k ∈ [ 1 , j − 1 ] , a i = b j
每次遍历通过维护一个前缀最大值的方案可以省去一维的时间复杂度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ll n; ll a[MAXN]; ll b[MAXN]; ll dp[MAXN][MAXN]; void solve () { n = read (); for (int i = 1 ; i <= n; i++) a[i] = read (); for (int i = 1 ; i <= n; i++) b[i] = read (); ll mx = 0 ; for (int i = 1 ; i <= n; i++) { ll mx = 0 ; for (int j = 1 ; j <= n; j++) { dp[i][j] = dp[i - 1 ][j]; if (a[i] == b[j]) dp[i][j] = max (mx + 1 , dp[i][j]); if (b[j] < a[i]) mx = max (mx, dp[i - 1 ][j]); } } ll ans = 0 ; for (int i = 1 ; i <= n; i++) ans = max (ans, dp[n][i]); printf ("%lld\n" , ans); }
背包模型
01背包求最值
1 2 3 4 5 6 7 8 9 10 11 for (int i = 1 ; i <= n; i++) for (int j = V; j >= v[i]; j--) dp[j] = max (dp[j], dp[j - v[i]] + w[i]); for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= V; j++) { dp[i][j] = dp[i - 1 ][j]; if (j >= v[i]) dp[i][j] = max (dp[i][j], dp[i - 1 ][j - v[i]] + w[i]); } }
二维费用背包(就比01背包多开了一维)
1 2 3 4 for (int i = 1 ; i <= n; i++) for (int j = V; j >= v[i]; j--) for (int k = M; k >= m[i]; k--) dp[j][k] = max (dp[j][k], dp[j - v[i]][k - m[i]] + w[i]);
01背包求方案数(记得初始化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ll n, V; ll v[MAXN]; ll dp[MAXM]; void solve () { n = read (), V = read (); dp[0 ] = 1 ; for (int i = 1 ; i <= n; i++) v[i] = read (); for (int i = 1 ; i <= n; i++) { for (int j = V; j >= v[i]; j--) { dp[j] += dp[j - v[i]]; } } printf ("%lld\n" , dp[V]); }
完全背包求最值(可以无限次选择)
遍历 j j j 的时候从倒着遍历改成正的遍历就行了
完全背包求方案数。同求最值,改成正着遍历;注意初始化。
多重背包(单调队列优化)
物品数量有限,求最大价值。
价值 w [ i ] w[i] w [ i ] ,体积 v [ i ] v[i] v [ i ] ,个数 s [ i ] s[i] s [ i ]
令 r = j r = j r = j m o d mod m o d v i v_i v i ,也可以理解为完全背包下把当前物品选到不能再选后,剩下的余数。
可以推出公式 d p ( i , r ) = d p ( i − 1 , r ) dp(i,r)=dp(i-1,r) d p ( i , r ) = d p ( i − 1 , r ) 。
下述公式的第一个维度省略掉了,因为之和前一维有关
d p ( i , r ) = d p r dp(i,r) = dp_r d p ( i , r ) = d p r
d p ( i , r + v ) = m a x ( d p r + v , d p r + w ) dp(i,r+v) = max(dp_{r+v},dp_r+w) d p ( i , r + v ) = m a x ( d p r + v , d p r + w )
d p ( i , r + 2 v ) = m a x ( d p r + 2 v , d p r + v + w , d p r + 2 w ) dp(i,r+2v) = max(dp_{r+2v},dp_{r+v}+w,dp_r+2w) d p ( i , r + 2 v ) = m a x ( d p r + 2 v , d p r + v + w , d p r + 2 w )
…
d p ( i , r + s v ) = m a x ( d p r + s v , d p r + ( s − 1 ) v + w , . . . , d p r + s w ) dp(i,r+sv)=max(dp_{r+sv},dp_{r+(s-1)v} +w,...,dp_r+sw) d p ( i , r + s v ) = m a x ( d p r + s v , d p r + ( s − 1 ) v + w , . . . , d p r + s w ) 滑动窗口已满
d p ( i , r + ( s + 1 ) v ) = m a x ( d p r + ( s + 1 ) v , d p r + s v + w , . . . , d p r + v + s w ) dp(i,r+(s+1)v)=max(dp_{r+(s+1)v},dp_{r+sv}+w,...,dp_{r+v}+sw) d p ( i , r + ( s + 1 ) v ) = m a x ( d p r + ( s + 1 ) v , d p r + s v + w , . . . , d p r + v + s w ) 滑动窗口已满
…
d p ( i , j − 2 v ) = m a x ( d p j − 2 v , d p j − 3 v + w , . . . , d p j − ( s + 2 ) v + s w ) dp(i,j-2v)=max(dp_{j-2v},dp_{j-3v}+w,...,dp_{j-(s+2)v}+sw) d p ( i , j − 2 v ) = m a x ( d p j − 2 v , d p j − 3 v + w , . . . , d p j − ( s + 2 ) v + s w )
d p ( i , j − v ) = m a x ( d p j − v , d p j − 2 v + w , . . . , d p j − ( s + 1 ) v + s w ) dp(i,j-v)=max(dp_{j-v},dp{j-2v}+w,...,dp_{j-(s+1)v}+sw) d p ( i , j − v ) = m a x ( d p j − v , d p j − 2 v + w , . . . , d p j − ( s + 1 ) v + s w )
d p ( i , j ) = m a x ( d p j , d p j − v + w , . . . , d p j − s w + s w ) dp(i,j)=max(dp_j,dp_{j-v}+w,...,dp_{j-sw}+sw) d p ( i , j ) = m a x ( d p j , d p j − v + w , . . . , d p j − s w + s w )
由此可见,滑动窗口大小为 s [ i ] + 1 s[i]+1 s [ i ] + 1
通过该滑动窗口,就能在线性的时间里求出 i i i 阶段中,所有满足 j = r j=r j = r m o d mod m o d v [ i ] v[i] v [ i ] 的 d p ( i , j ) dp(i,j) d p ( i , j )
滑动窗口求最大值的实现,可以利用维护一个最大值单调递减的单调队列。
枚举所有的余数 r r r 即可 ( 0 , v [ i ] − 1 ) (0,v[i]-1) ( 0 , v [ i ] − 1 )
不要忘记比较使得偏移量 w w w 。
具体就是当前下标和最大值的下标之间差了 x x x 个 v [ i ] v[i] v [ i ] ,那么就要加上 x x x 个 w [ i ] w[i] w [ i ]
时间复杂度 O ( n × v ) O(n \times v) O ( n × v ) 空间复杂度 O ( n × v ) O(n \times v) O ( n × v ) 滑动窗口长度 s [ i ] + 1 s[i]+1 s [ i ] + 1
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 ll v[MAXN], w[MAXN], s[MAXN], V, n; ll q[MAXM]; ll dp[2 ][MAXM]; void solve () { n = read (); V = read (); for (int i = 1 ; i <= n; i++) { v[i] = read (); w[i] = read (); s[i] = read (); } for (int i = 1 ; i <= n; i++) { for (int r = 0 ; r < v[i]; r++) { int hh = 1 , tt = 0 ; for (int j = r; j <= V; j += v[i]) { int now = i & 1 ; int pre = (i - 1 ) & 1 ; while (hh <= tt && dp[pre][q[tt]] + (j - q[tt]) / v[i] * w[i] <= dp[pre][j]) tt--; q[++tt] = j; while (hh <= tt && j - q[hh] > v[i] * s[i]) hh++; dp[now][j] = dp[pre][q[hh]] + (j - q[hh]) / v[i] * w[i]; } } } printf ("%lld\n" , dp[n & 1 ][V]); }
多重背包(二进制优化)
这个就好理解多了。所有数都可以被看成 1 + 2 + 4 + . . . + n − 2 k + 1 1+2+4+...+n-2^k+1 1 + 2 + 4 + . . . + n − 2 k + 1 。这样的话 s [ i ] = 1 + 2 + 4 + . . . + 2 k − 1 , s [ i ] − ( 2 k − 1 ) s[i] = 1 + 2+4+...+2^{k-1},s[i]-(2^k-1) s [ i ] = 1 + 2 + 4 + . . . + 2 k − 1 , s [ i ] − ( 2 k − 1 ) 。那么就可以当成若干个01背包中的物品了。体积是 x × v x \times v x × v ,价值是 x × w x \times w x × w
时间复杂度 O ( n 2 l o g s ) O(n^2logs) O ( n 2 l o g s ) ,其中 s s s 表示所有物品的数量之和
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 ll n, V; ll v[MAXN], w[MAXN], s[MAXN]; ll dp[MAXM]; void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) { v[i] = read (); w[i] = read (); s[i] = read (); } for (int i = 1 ; i <= n; i++) { for (int k = 1 ; k <= s[i]; k *= 2 ) { ll tmpv = k * v[i]; ll tmpw = k * w[i]; for (int j = V; j >= tmpv; j--) dp[j] = max (dp[j - tmpv] + tmpw, dp[j]); s[i] -= k; } if (s[i]) { ll tmpv = s[i] * v[i]; ll tmpw = s[i] * w[i]; for (int j = V; j >= tmpv; j--) dp[j] = max (dp[j - tmpv] + tmpw, dp[j]); } } printf ("%lld\n" , dp[V]); }
混合背包
物品分三类:只能取一次,能取无限次,能取 s [ i ] s[i] s [ i ] 次。使总价值最大。
针对三类物品做不同的状态转移即可,具体参考前文。
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 ll n, V; ll v[MAXN], w[MAXN], s[MAXN]; ll dp[MAXN]; void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) { v[i] = read (); w[i] = read (); s[i] = read (); } for (int i = 1 ; i <= n; i++) { if (!s[i]) for (int j = v[i]; j <= V; j++) dp[j] = max (dp[j], dp[j - v[i]] + w[i]); else if (s[i] == -1 ) for (int j = V; j >= v[i]; j--) dp[j] = max (dp[j], dp[j - v[i]] + w[i]); else { for (int k = 1 ; k <= s[i]; k *= 2 ) { for (int j = V; j >= k * v[i]; j--) dp[j] = max (dp[j], dp[j - k * v[i]] + k * w[i]); s[i] -= k; } if (s[i]) for (int j = V; j >= s[i] * v[i]; j--) dp[j] = max (dp[j], dp[j - s[i] * v[i]] + s[i] * w[i]); } } printf ("%lld\n" , dp[V]); }
一些变形
可以放置超过背包容量的物品,体积至少为xxx:
即当前背包预留的容量可以由负数转移过来(看成0)
1 2 3 4 5 6 memset (dp, 0x3f , sizeof dp);dp[0 ][0 ] = 0 ; for (int i = 1 ; i <= n; i++) for (int j = V; j >= 0 ; j--) for (int k = M; k >= 0 ; k--) dp[j][k] = min (dp[j][k], dp[max (0ll , j - v[i])][max (0ll , k - m[i])] + w[i]);
体积恰好为xxx
改变状态转移方程的前提条件。要么之前 i − 1 i-1 i − 1 时当前这个体积已经有方案了,要么只能有体积为 0 0 0 的状态转移过来。
1 2 3 4 5 if (j >= v[i] && (j == v[i] || dp[i - 1 ][j - v[i]]) != 0 ) dp[i][j] = (dp[i][j] + dp[i - 1 ][j - v[i]]) % mod; if (j == v[i] / 2 || dp[i - 1 ][j - v[i] / 2 ] != 0 ) dp[i][j] = (dp[i][j] + dp[i - 1 ][j - v[i] / 2 ]) % mod;
输出方案
有 n n n 件物品和一个容量为 V V V 的背包。第 i i i 件物品的体积是 v i v_i v i ,价值是 w i w_i w i ,每件物品只能用一次。求解一种方案,使得选择的物品总体积小于 V V V ,且总价值最大,输出字典序最小的方案。
做法就是先做一遍正常的背包 D P DP D P ,然后从目标状态倒退回初始状态的整个转移路径
伪代码:
1 2 3 4 5 6 7 8 9 10 int v = V; for (从最后一件循环至第一件) { if (g[i][v]) { 选了第 i 项物品; v -= 第 i 项物品的价值; } else 未选第 i 项物品; }
题目还要求了字典序最小。在倒退转移方程时,如果碰到物品既可以选也可以不选,优先的选项是选择该物品。因此,背包 D P DP D P 是倒过来 (从n到1),然后再从 1 1 1 倒推会 n n n 找出路径。这样,如果出现分叉情况是,就优先选当前物品即可。
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 33 34 35 ll n, V; ll dp[MAXN][MAXN]; ll v[MAXN], w[MAXN]; int path[MAXN], cnt = 0 ; void dfs (int i, ll j) { if (i == n + 1 ) return ; if (j >= v[i] && dp[i][j] == dp[i + 1 ][j - v[i]] + w[i]) { path[++cnt] = i; dfs (i + 1 , j - v[i]); } else dfs (i + 1 , j); } void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) v[i] = read (), w[i] = read (); for (int i = n; i >= 1 ; i--) { for (int j = 0 ; j <= V; j++) { dp[i][j] = dp[i + 1 ][j]; if (j >= v[i]) dp[i][j] = max (dp[i][j], dp[i + 1 ][j - v[i]] + w[i]); } } dfs (1 , V); for (int i = 1 ; i <= cnt; i++) printf ("%d " , path[i]); }
分组背包+输出方案
题目:总公司拥有 M M M 台设备,准备分给下属的 n n n 个分公司。第 i i i 家公司分到 j j j 台机器后,所获得的收益为 w i j w_{ij} w i j 。求一种分配方案,使得总收益最大,输出该方案。
分析:每家公司都可以看成一个物品组,又因为每家公司最终能够被分配的机器数量是固定的,因此对于分给第 i i i 个公司的不同机器数量可以分别看做是一个物品组内的物品。
物品含义:分为第 i i i 个公司 k k k 台机器
物品体积:k k k
该物品 k k k 的价值:w i k w_{ik} w i k
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 33 34 35 36 37 38 39 40 41 42 ll n, m; ll a[25 ][25 ]; ll dp[25 ][25 ]; int path[25 ], cnt;void dfs (int now, int v) { if (!now) return ; for (int i = 0 ; i <= m; i++) { if (v >= i && dp[now][v] == dp[now - 1 ][v - i] + a[now][i]) { path[now] = i; dfs (now - 1 , v - i); return ; } } } void solve () { n = read (), m = read (); for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) a[i][j] = read (); for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { dp[i][j] = dp[i - 1 ][j]; for (int k = 1 ; k <= j; k++) { dp[i][j] = max (dp[i][j], dp[i - 1 ][j - k] + a[i][k]); } } } printf ("%lld\n" , dp[n][m]); dfs (n, m); for (int i = 1 ; i <= n; i++) printf ("%d %d\n" , i, path[i]); }
这是分组背包最裸的模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ll n, V; ll v[MAXN][MAXN], w[MAXN][MAXN], dp[MAXN][MAXN], cnt[MAXN]; void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) { cnt[i] = read (); for (int j = 1 ; j <= cnt[i]; j++) v[i][j] = read (), w[i][j] = read (); } for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= V; j++) { dp[i][j] = dp[i - 1 ][j]; for (int k = 1 ; k <= cnt[i]; k++) { if (j >= v[i][k]) dp[i][j] = max (dp[i][j], dp[i - 1 ][j - v[i][k]] + w[i][k]); } } } printf ("%lld\n" , dp[n][V]); }
压缩成一维后:
1 2 3 4 5 6 for (int i = 1 ; i <= n; i++) for (int j = V; j >= 0 ; j--) for (int k = 1 ; k <= cnt[i]; k++) if (j >= v[i][k]) dp[j] = max (dp[j], dp[j - v[i][k]] + w[i][k]); printf ("%lld\n" , dp[V]);
01背包求最优方案数
思路就是先求一次 d p dp d p ,再搞一次逆推方案。
空间不优化写法:
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 ll n, V; ll v[MAXN], w[MAXN]; ll mx; ll dp[MAXN][MAXN]; ll cnt[MAXN][MAXN]; void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) v[i] = read (), w[i] = read (); for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= V; j++) { dp[i][j] = dp[i - 1 ][j]; if (j >= v[i]) dp[i][j] = max (dp[i][j], dp[i - 1 ][j - v[i]] + w[i]); } } cnt[0 ][0 ] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= V; j++) { if (dp[i][j] == dp[i - 1 ][j]) cnt[i][j] = (cnt[i][j] + cnt[i - 1 ][j]) % mod; if (j >= v[i] && dp[i - 1 ][j - v[i]] + w[i] == dp[i][j]) cnt[i][j] = (cnt[i][j] + cnt[i - 1 ][j - v[i]]) % mod; } } ll ans = 0 ; for (int i = 0 ; i <= V; i++) if (dp[n][i] == dp[n][V]) ans = (ans + cnt[n][i]) % mod; printf ("%lld\n" , ans); }
空间优化成一维后:
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 ll n, V; ll v[MAXN], w[MAXN]; ll mx; ll dp[MAXN]; ll cnt[MAXN]; void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) v[i] = read (), w[i] = read (); cnt[0 ] = 1 ; for (int i = 1 ; i <= n; i++) { for (int j = V; j >= v[i]; j--) { ll tmp = max (dp[j], dp[j - v[i]] + w[i]); ll now = 0 ; if (tmp == dp[j - v[i]] + w[i]) now = (now + cnt[j - v[i]]) % mod; if (tmp == dp[j]) now = (now + cnt[j]) % mod; dp[j] = tmp, cnt[j] = now; } } ll ans = 0 ; for (int i = 0 ; i <= V; i++) if (dp[i] == dp[V]) ans = (ans + cnt[i]) % mod; printf ("%lld\n" , ans); }
有依赖的背包问题(树上背包)
有 n n n 件物品和一个体积为 V V V 的背包。第 i i i 件物品的体积为 v [ i ] v[i] v [ i ] ,价值为 w [ i ] w[i] w [ i ] ,每件物品有一个父节点物品 p [ i ] p[i] p [ i ] ,如果想选第 i i i 件物品,就必须选他的父节点 p [ i ] p[i] p [ i ] 。求能选出的最大价值。
首先,这种依赖关系类似于树中的父节点和子节点,于是用树形DP来做。
考虑到根据方案划分的话,有 2 x 2^x 2 x 种状态,显然存不下。因此考虑根据体积来划分,枚举每棵子树的共用体积。(这个过程有点像分组背包)
状态表示集合:考虑第 i i i 个物品为根节点的子树,且选上 i i i ,选法的总体积不超过 j j j 的方案。
状态表示属性:方案的总价值最大 M a x Max M a x
状态计算:
时间复杂度 O ( n × V × V ) O(n\times V\times V) O ( n × V × V )
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 ll n, V; ll v[MAXN], w[MAXN]; ll dp[MAXN][MAXN]; int root;void dfs (int now, int pre) { for (int i = head[now]; i; i = edge[i].next) { int to = edge[i].to; if (to == pre) continue ; dfs (to, now); for (int j = V - v[now]; j >= 0 ; j--) for (int k = 0 ; k <= j; k++) dp[now][j] = max (dp[now][j], dp[now][j - k] + dp[to][k]); } for (int j = V; j >= v[now]; j--) dp[now][j] = dp[now][j - v[now]] + w[now]; for (int j = 0 ; j < v[now]; j++) dp[now][j] = 0 ; } void solve () { n = read (), V = read (); for (int i = 1 ; i <= n; i++) { int fa; v[i] = read (), w[i] = read (), fa = read (); if (fa == -1 ) root = i; else add_edge (fa, i), add_edge (i, fa); } dfs (root, -1 ); printf ("%lld\n" , dp[root][V]); }
亿点点细节:枚举子节点的共用体积 j j j 时要倒着枚举,因为确保当前一层只被更新一次。根节点物品最后才计算,因为这样能防止被覆盖掉。体积小于 v [ n o w ] v[now] v [ n o w ] 的要被清零,因为这是有依赖关系的,子节点生效的前提条件是选了当前父节点。
有依赖的背包问题(体积和价值在边上)
给定一颗含有 n n n 个节点的树,树根编号为 1 1 1 ,且树上的每一条边有一个边权 w [ i ] w[i] w [ i ] 。要求保留树中的 m m m 条边,使得树根所在的连通块的所有边边权之和最大。
定义状态d p [ i ] [ j ] dp[i][j] d p [ i ] [ j ] ,以 i i i 为根节点的子树,包含 i i i 的连通块的边数不超过 j j j 的方案。
当点作为体积时,第一重循环的 j j j 可以取 V − v [ n o w ] V - v[now] V − v [ n o w ] 也可以取 V V V ,因为根据状态的定义,必须选当前节点的体积,另外多枚举的部分在之后会被覆盖掉。当边作为体积时, j j j 只能从 V V V 开始而不是 V − 1 V -1 V − 1 ,因为假设当前是根节点的话就少枚举了。
k k k 则对应决策方案,为某个子树被分到的体积。枚举 k k k 的时候最大到 j − 1 j - 1 j − 1 ,是因为如果计算该子树对父节点的贡献,必须保留他到父节点的边。
代码可以和上一道好好对比
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 33 34 ll n, m; struct EDGE { int to, next, val; }edge[MAXM]; int head[MAXN], tot;void add (int from, int to, int val) { edge[++tot].to = to, edge[tot].val = val, edge[tot].next = head[from], head[from] = tot; } ll dp[MAXN][MAXN]; void dfs (int now, int pre) { for (int i = head[now]; i; i = edge[i].next) { int to = edge[i].to; int val = edge[i].val; if (to == pre) continue ; dfs (to, now); for (int j = m; j >= 0 ; j--) { for (int k = 0 ; k < j; k++) { dp[now][j] = max (dp[now][j - k - 1 ] + dp[to][k] + val, dp[now][j]); } } } } void solve () { n = read (), m = read (); for (int i = 1 ; i < n; i++) { int u = read (), v = read (), w = read (); add (u, v, w); add (v, u, w); } dfs (1 , -1 ); printf ("%lld\n" , dp[1 ][m]); }
有依赖的背包问题,根节点不止一个(森林)
可以利用图论中超级源点中的思想,建一个虚拟源点连接所有的根节点,这样森林就变成了一棵树。然后定义这个虚拟源点的体积为 1 1 1 ,或者任何正数,在总体积中加上虚拟源点的体积正常做树上背包即可。例题中所有节点体积都为 1 1 1 。
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 int n, V;int dp[MAXN][MAXN];int w[MAXN];void dfs (int now, int pre) { for (int i = head[now]; i; i = edge[i].next) { int to = edge[i].to; if (to == pre) continue ; dfs (to, now); for (int j = V; j >= 0 ; j--) for (int k = 0 ; k <= j; k++) dp[now][j] = max (dp[now][j - k] + dp[to][k], dp[now][j]); } for (int j = V; j >= 1 ; j--) dp[now][j] = dp[now][j - 1 ] + w[now]; dp[now][0 ] = 0 ; } void solve () { n = read (), V = read (); V++; for (int i = 1 ; i <= n; i++) { int fa; fa = read (), w[i] = read (); add (fa, i), add (i, fa); } dfs (0 , -1 ); printf ("%d\n" , dp[0 ][V]); }
有依赖的背包问题(分组背包)
一共有 n n n 个物品和 V V V 体积的背包。
物品之间可能存在依赖关系,每个物品体积为 v [ i ] v[i] v [ i ] , 价值为 w [ i ] w[i] w [ i ] ,依赖的父亲物品为 p [ i ] p[i] p [ i ] ,每个物品只能被购买一次。
求一种购买方案,使得总花费不超过 V V V ,且总价值最大。
注意,每个父亲的儿子不超过两个,且儿子不会再有儿子。
如果按照体积划分,时间复杂度 O ( n × V × V ) O(n \times V \times V) O ( n × V × V ) 就会超时了。
注意到每个主件的附属品不超过两个,且附属品不会再有附属品。因此可以采用分组背包对本题的状态进行划分。具体做法就是用类似于状态压缩的方法,二进制枚举所有情况,每种组合对应一个物品组内的一个物品。时间复杂度 O ( n × 2 2 × V ) O(n \times 2^2 \times V) O ( n × 2 2 × V )
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 33 34 35 36 37 38 39 40 41 ll V, n; ll v[MAXN], w[MAXN]; bool isfa[MAXN]; vector<int > g[MAXN]; ll dp[MAXM]; void init () { for (int i = 1 ; i <= n; i++) g[i].clear (), isfa[i] = false ; } void work (int now, ll j) { int len = g[now].size (); for (int st = 0 ; st < (1 << len); st++) { int sum_v = v[now]; ll sum_w = w[now]; for (int i = 0 ; i < len; i++) { if (st >> i & 1 ) sum_v += v[g[now][i]], sum_w += w[g[now][i]]; } if (sum_v <= j) dp[j] = max (dp[j], dp[j - sum_v] + sum_w); } } void solve () { V = read (), n = read (); init (); for (int i = 1 ; i <= n; i++) { int fa; v[i] = read (), w[i] = read (), fa = read (); w[i] = w[i] * v[i]; if (fa) g[fa].pb (i); else isfa[i] = true ; } for (int i = 1 ; i <= n; i++) if (isfa[i]) for (int j = V; j >= v[i]; j--) work (i, j); printf ("%lld\n" , dp[V]); }