01背包
完全背包
多重背包
分组背包
混合背包
对于物品而言只能选择1个或者0个两种情况
对于物品而言可以无限制选取,也可以不选
对于物品而言最多能够选择从s[i]个,同样也可不选
一些物品捆绑在一起,每一组物品中只能选择其中的一个物品
有些物品可以选择1,有些物品可以选择无数个,有些物品只能选择是s[i]个.即:01背包+完全背包+多重背包.
滚动数组
滚动数组
滚动数组
滚动数组
滚动数组
二进制优化或者单调队列优化
其中多重背包部分参考多重背包优化
0 - 1 背包问题
有 N N 件物品和一个容量是 V V 的背包。每件物品只能使用一次。
第 i i 件物品的体积是 v**i vi,价值是 w**i wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
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 int n, m;int v[N], w[N]; int f[N][N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> v[i] >> w[i]; for (int i = 1 ; i <= n; ++i) for (int j = 1 ; j<=m; ++j) { if (j<w[i]) f[i][j] = f[i-1 ][j]; else f[i][j] = max (f[i-1 ][j], f[i-1 ][j-w[i]]+v[i]); } cout << f[n][m] << endl; return 0 ; } int f[N]; int main () { cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> 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]); cout << f[m] << endl; return 0 ; }
完全背包问题
首先dp数组初始化全为0:给定物品种类有4种,包最大体积为5,数据来源于题目的输入
v[1] = 1, w[1] = 2
v[2] = 2, w[2] = 4
v[3] = 3, w[3] = 4
v[4] = 4, w[4] = 5
i = 1 时: j从v[1]到5
dp[1] = max(dp[1],dp[0]+w[1]) = w[1] = 2 (用了一件物品1)
dp[2] = max(dp[2],dp[1]+w[1]) = w[1] + w[1] = 4(用了两件物品1)
dp[3] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] = 6(用了三件物品1)
dp[4] = max(dp[4],dp[3]+w[1]) = w[1] + w[1] + w[1] + w[1] = 8(用了四件物品1)
dp[5] = max(dp[3],dp[2]+w[1]) = w[1] + w[1] + w[1] + w[1] + w[1] = 10(用了五件物品)
i = 2 时:j从v[2]到5
dp[2] = max(dp[2],dp[0]+w[2]) = w[1] + w[1] = w[2] = 4(用了两件物品1或者一件物品2)
dp[3] = max(dp[3],dp[1]+w[2]) = 3 * w[1] = w[1] + w[2] = 6(用了三件物品1,或者一件物品1和一件物品2)
dp[4] = max(dp[4],dp[2]+w[2]) = 4 * w[1] = dp[2] + w[2] = 8(用了四件物品1或者,两件物品1和一件物品2或两件物品2)
dp[5] = max(dp[5],dp[3]+w[2]) = 5 * w[1] = dp[3] + w[2] = 10(用了五件物品1或者,三件物品1和一件物品2或一件物品1和两件物品2)
i = 3时:j从v[3]到5
dp[3] = max(dp[3],dp[0]+w[3]) = dp[3] = 6 # 保持第二轮的状态
dp[4] = max(dp[4],dp[1]+w[3]) = dp[4] = 8 # 保持第二轮的状态
dp[5] = max(dp[5],dp[2]+w[3]) = dp[4] = 10 # 保持第二轮的状态
i = 4时:j从v[4]到5
dp[4] = max(dp[4],dp[0]+w[4]) = dp[4] = 10 # 保持第三轮的状态
dp[5] = max(dp[5],dp[1]+w[4]) = dp[5] = 10 # 保持第三轮的状态
上面模拟了完全背包的全部过程,也可以看出,最后一轮的dp[m]即为最终的返回结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for (int i = 0 ; i < n; i++) { for (int j = m; j >= v[i]; j--) { for (int k = 0 ; k * v[i] < j; k++) { f[j] = max (f[j], f[j-v[i]*k] + k*w[i]); } } } int n,m, arr[N],v[N],w[N];int main () { cin >> n >> m; for (int i = 1 ;i <= n;i++){ cin >> v[i] >> w[i]; } for (int i = 1 ;i<=n;i++){ for (int j = v[i];j <= m ;j++){ arr[j] = max (arr[j] , arr[j-v[i]]+w[i]); } } cout << arr[m]; return 0 ; }
分组背包问题
有 N 组物品和一个容量是 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 vi j vij,价值是 wi j wij,其中 i i 是组号,j j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int n, m, v[N][N], w[N][N], s[N], f[N];int main () { cin >> n >> m; for (int i = 1 ; i <= n; i ++ ) { cin >> s[i]; for (int j = 0 ; j < s[i]; j ++ ) cin >> v[i][j] >> w[i][j]; } for (int i = 1 ; i <= n; i ++ ) for (int j = m; j >= 0 ; j -- ) for (int k = 0 ; k < s[i]; k ++ ) if (v[i][k] <= j) f[j] = max (f[j], f[j - v[i][k]] + w[i][k]); cout << f[m] << endl; return 0 ; }
多重背包问题
有 N 种物品和一个容量是 V的背包。第 i 种物品最多有 s 件,每件体积是 v**i vi,价值是 w**i wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
1 2 3 4 5 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 - v[i] * k] + w[i] * k);
记忆化搜索
给定一个R行C列的矩阵,表示一个矩形网格滑雪场。矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。下面给出一个矩阵作为例子:
在给定矩阵中,一条可行的滑行轨迹为24-17-2-1。在给定矩阵中,最长的滑行轨迹为25-24-23-…-3-2-1,沿途共经过25个区域。现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
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 const int N = 310 ;int n, m;int g[N][N];int f[N][N];int dx[4 ] = {-1 , 0 , 1 , 0 }, dy[4 ] = {0 , 1 , 0 , -1 }; int dp (int x, int y) { int &v = f[x][y]; if (v != -1 ) return v; v = 1 ; for (int i = 0 ; i < 4 ; i ++ ) { int a = x + dx[i], b = y + dy[i]; if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b]) v = max (v, dp (a, b) + 1 ); } return v; } int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= m; j ++ ) scanf ("%d" , &g[i][j]); memset (f, -1 , sizeof f); int res = 0 ; for (int i = 1 ; i <= n; i ++ ) for (int j = 1 ; j <= m; j ++ ) res = max (res, dp (i, j)); printf ("%d\n" , res); return 0 ; }
树形DP: 没有上司的舞会
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 const int N = 6010 ;int n;int h[N], e[N], ne[N], idx;int happy[N];int f[N][2 ];bool has_fa[N]; void add (int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; } void dfs (int u) { f[u][1 ] = happy[u]; for (int i = h[u]; ~i; i = ne[i]) { int j = e[i]; dfs (j); f[u][1 ] += f[j][0 ]; f[u][0 ] += max (f[j][0 ], f[j][1 ]); } } int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i ++ ) scanf ("%d" , &happy[i]); memset (h, -1 , sizeof h); for (int i = 0 ; i < n - 1 ; i ++ ) { int a, b; scanf ("%d%d" , &a, &b); add (b, a); has_fa[a] = true ; } int root = 1 ; while (has_fa[root]) root ++ ; dfs (root); printf ("%d\n" , max (f[root][0 ], f[root][1 ])); return 0 ; }
状态压缩:蒙德里安的梦想
求把 N 乘 M 的棋盘分割成若干个1*2的的长方形,有多少种方案。
例如当N=2,M=4时,共有5种方案。当N=2,M=3时,共有3种方案。
如下图所示:
解题思路: 按列摆放,分为横向10瓷砖、纵向00瓷砖。假设此时已将所有横向摆放的瓷砖排列完全,此时就仅剩下纵向格子的摆放位置,因此,对于某一列来说,本行与上一行同为0的瓷砖,不能为奇数个,不然这样就不能接着放下纵向瓷砖了。比如说现在有3行全0列,剩下一行,什么也放不了。其次,相邻的两列不能均为1,如果出现的话,两个横向摆放瓷砖冲突。最后,由于横向10,纵向00的布局,导致第m列只能是全0,状态存储至f [m] [0]。实际状态是0到m列。
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 const int N = 1 << 12 ;long long f[12 ][N];bool st[N];int n, m;int main () { int n, m; while (cin >> n >> m, n || m) { memset (f, 0 , sizeof f); for (int i = 0 ; i < 1 << n; i++) { int ans = 0 ; st[i] = true ; for (int k = 0 ; k < n; k++) { if (i>>k & 1 ) { if (ans&1 ) st[i] = false ; } else ans++; } if (ans&1 ) st[i] = false ; } f[0 ][0 ] = 1 ; for (int i = 1 ; i <= m; i++) { for (int k = 0 ; k < N; k++) { for (int j = 0 ; j < N; j++) { if ((k&j)==0 && st[k|j]) { f[i][k] += f[i-1 ][j]; } } } } cout << f[m][0 ] << endl; } }
状态压缩:最短Hamilton路径
题目 :给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-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 using namespace std;const int N = 1 << 20 , M = 20 ;int f[N][M], weight[M][M];int n;int main () { cin >> n; for (int i = 0 ; i < n; i++) { for (int j = 0 ; j < n; j++) { cin >> weight[i][j]; } } memset (f, 0x3f , sizeof f); f[1 ][0 ] = 0 ; for (int i = 1 ; i < 1 <<n; i++) { for (int j = 0 ; j < n; j++) { if ((i >> j) & 1 ) { for (int k = 0 ; k < n; k++) { if ((i - (1 << j)) >> k & 1 ) { f[i][j] = min (f[i][j], f[i - (1 << j)][k] + weight[k][j]); } } } } } cout << f[(1 << n) - 1 ][n - 1 ] << endl; return 0 ; }
线性DP:数字三角形
题目 :给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
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 using namespace std;const int N = 500 , INF = 1e9 ;int path[N][N], w[N][N]; int n;int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= i; j++) { scanf ("%d" , &path[i][j]); } } for (int i = 0 ; i <= n; i++) { for (int j = 0 ; j <= i + 1 ; j++) { w[i][j] = -INF; } } w[1 ][1 ] = path[1 ][1 ]; for (int i = 2 ; i <= n; i++) { for (int j = 1 ; j <= i; j++) { w[i][j] = max (w[i-1 ][j], w[i-1 ][j-1 ]) + path[i][j]; } } int ans = -INF; for (int i = 1 ; i <= n; i++) ans = max (ans, w[n][i]); cout << ans; return 0 ; }
线性DP:最长上升公共子序列
题目 :给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 using namespace std;const int N = 1000 ;int n, res = -0x3f ;int f[N], a[N];int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) { scanf ("%d" , &a[i]); } memset (f, 0 , sizeof f); for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j < i; j++) { if (a[j] < a[i]) f[i] = max (f[j], f[i]); } f[i] += 1 ; } for (int i = 1 ; i <= n; i++) { res = max (res, f[i]); } cout << res; return 0 ; }
思路二 :我之前好像尝试了使用单调队列做,但是没有明白如果遇到元素冲突应该怎么解决,然后看了一个题解,发现一个很厉害的做法。
枚举数组里的数,如果插入值大于队列,就插在末尾,反之,就按查找到序号往左替换队列的值,时间复杂度是对数级。队列搞个负无穷来初始化也是为了防止数据出现空集的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 【测试数组】(在样例上多加了两个数) [10 , 9 , 2 , 5 , 3 , 7 , 101 , 18 , 4 , 19 ] 【有效长度/队列/插入值】 1 [-inf, 10 ] <- 10 1 [-inf, 9 ] <- 9 1 [-inf, 2 ] <- 2 2 [-inf, 2 , 5 ] <- 5 2 [-inf, 2 , 3 ] <- 3 3 [-inf, 2 , 3 , 7 ] <- 7 4 [-inf, 2 , 3 , 7 , 101 ] <- 101 4 [-inf, 2 , 3 , 7 , 18 ] <- 18 4 [-inf, 2 , 3 , 4 , 18 ] <- 4 5 [-inf, 2 , 3 , 4 , 18 , 19 ] <- 19
线性DP:最长上升公共子序列II
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 const int N = 1e5 ; int f[N], a[N]; int n;int main () { scanf ("%d" , &n); for (int i = 0 ; i < n; i++) { scanf ("%d" , &a[i]); } for (int i = 0 ; i < n; i++) { f[i] = -0x3f ; } int maxLen = 0 ; for (int i = 0 ; i < n; i++) { int l = 0 ,r = maxLen; while (l < r) { int mid = (l + r + 1 ) >> 1 ; if (f[mid] < a[i]) l = mid; else r = mid - 1 ; } maxLen = max (r + 1 , maxLen); f[r + 1 ] = a[i]; } printf ("%d" , maxLen); return 0 ; }
线性DP:最长公共子序列
题目 :给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 char a[N], b[N];int n, m;int f[N][N];int main () { scanf ("%d %d" , &n, &m); scanf ("%s %s" , a+1 , b+1 ); for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j++) { f[i][j] = max (f[i-1 ][j], f[i][j-1 ]); if (a[i] == b[j]) f[i][j] = max (f[i][j], f[i-1 ][j-1 ] + 1 ); } } cout << f[n][m]; return 0 ; }
线性DP:编辑最短距离
**题目:**给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:现在请你求出,将A变为B至少需要进行多少次操作。
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 const int N = 1010 ;char a[N], b[N];int n, m;int f[N][N]; int main () { scanf ("%d %s" , &n, a + 1 ); scanf ("%d %s" , &m, b + 1 ); for (int i = 0 ; i <= n; i++) f[i][0 ] = i; for (int i = 0 ; i <= m; i++) f[0 ][i] = i; for (int i = 1 ; i <= n; i++) { for (int j = 1 ; j <= m; j ++){ f[i][j] = min (f[i-1 ][j], f[i][j-1 ]) + 1 ; if (a[i] == b[j]) f[i][j] = min (f[i][j], f[i-1 ][j-1 ]); else f[i][j] = min (f[i][j], f[i-1 ][j-1 ] + 1 ); } } cout << f[n][m]; return 0 ; }
区间DP:石子合并
题目 :每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int s[N], f[N][N];int n;int main () { scanf ("%d" , &n); for (int i = 1 ; i <= n; i++) scanf ("%d" , &s[i]); for (int i = 1 ; i <= n; i++) s[i] += s[i-1 ]; for (int len = 2 ; len <= n; len++ ) { for (int i = 1 ; i + len - 1 <= n; i++ ) { int l = i, r = i + len - 1 ; f[l][r] = 1e8 ; for (int k = l; k < r; k++) { f[l][r] = min (f[l][r], f[l][k] + f[k+1 ][r] + s[r] - s[l-1 ]); } } } printf ("%d" , f[1 ][n]); return 0 ; }
计数统计DP:整数划分
题目 :现在给定一个正整数n,请你求出n共有多少种不同的划分方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int n;int f[N];int main () { cin >> n; f[0 ] = 1 ; for (int i = 1 ; i <= n; i ++ ) for (int j = i; j <= n; j ++ ) f[j] = (f[j] + f[j - i]) % mod; cout << f[n] << endl; return 0 ; }
知识点和代码均学习于Acwing: https://www.acwing.com/activity/