背包九讲
01背包
¶有
其中,第
求选择将哪些物品装入背包可使获得的价值总和最大。
问题简析
¶因为每件物品只有两种选择:放或不放入背包。且每件物品之间相互独立,即无论第
即枚举背包的剩余容量,在每个容量下,第
算法优化
¶在上述转移方程中,不难发现第一维始终是递增的,因此可以使用滚动数组。进一步的,注意到第二维也是具有单调性的,如果我们从
而在求
综上,仅需一个一维数组即可完成状态转移,空间复杂度为
初始化细节
¶01背包问题存在两个变种,仅需改变初始化方式即可求解:
恰好装满背包时可以获得到的最大价值
将
初始化为: .这样初始化相当于强制转移只能沿着“剩余容量恰好消耗完”的方向进行,若最后
不为 ,则说明可构造出一条转移路径使得背包容量恰好被消耗完,反之,则说明无解。无需恰好装满背包时,可以获得的最大价值
将
初始化为: .即转移时无需考虑剩余容量是否有多余。
小结
¶空间复杂度:
时间复杂度:
1234567891011121314const int MAX_C = 20000 + 10;int F[MAX_C];
int solution(int N, int C) { memset(F, 0, (C + 1) * sizeof(int));
for (int i = 0, V, W; i < N; ++i) { scanf("%d%d", &V, &W); for (int c = C; c >= V; --c) { F[c] = std::max(F[c], F[c - V] + W); } } return F[C];}
完全背包
¶有
其中,第
求选择将哪些物品装入背包可使获得的价值总和最大。
问题简析
¶将第
基于此,存在两个优化:
若两种物品
和 满足 且 ,则删除第 种物品,仅考虑第 种物品。因为如果最优解中选用了物品 ,那么把它取出来换成物品 放进去,得到的结果不会更差;将第
种物品拆分成 个 [4],其中 ,则无论最优结果种物品 选了多少个,都能保证存在这样的组合能选到这个数量(如果理解不了,可以想象成二进制表示法)。复杂度降为 .
算法优化
¶之前的分析中展示了可以通过拆分物品的方式将原问题转换为 01背包问题,其实原题和
01背包问题的差别只是在前
记
观察上面的转移方程,不难发现前面01背包-算法优化中提到的优化策略仍然适用,不同的是,
小结
¶空间复杂度:
时间复杂度:
1234567891011121314const int MAX_C = 20000 + 10;int F[MAX_C];
int solution(int N, int C) { memset(F, 0, (C + 1) * sizeof(int));
for (int i = 0, V, W; i < N; ++i) { scanf("%d%d", &V, &W); for (int c = V; c <= C; ++c) { F[c] = std::max(F[c], F[c - V] + W); } } return F[C];}
多重背包
¶有
其中,第
求选择将哪些物品装入背包可使获得的价值总和最大。
问题简析
¶可以延续完全背包的拆分思路,将第
算法优化
¶记
不妨记
由上述转移方程组,可以列出
可以发现,对于
则在求
小结
¶二进制优化
空间复杂度:
时间复杂度:多重背包-01.cpp | 21 lines.123456789101112131415161718192021const int MAX_C = 20000 + 10;int F[MAX_C];int solution(int N, int C) {memset(F, 0, (C + 1) * sizeof(int));for (int i = 0, V, W, M; i < N; ++i) {scanf("%d%d%d", &V, &W, &M);for (int k = 1; k <= M; k <<= 1) {int m = (k << 1) <= M ? k : M - k + 1;int V2 = m * V;if (V2 > C) break;int W2 = m * W;for (int c = C; c >= V2; --c) {F[c] = std::max(F[c], F[c - V2] + W2);}}}return F[C];}单调栈优化
空间复杂度:
时间复杂度:多重背包-02.cpp | 36 lines.123456789101112131415161718192021222324252627282930313233343536const int MAX_C = 20000 + 10;int g[MAX_C], h[MAX_C];int ms[MAX_C]; // 单调栈int solution(int N, int C) {memset(g, 0, sizeof(int) * (C + 1));for (int i = 0, V, W, M, ss, tt; i < N; ++i) {scanf("%d%d%d", &V, &W, &M);std::swap(g, h);for (int x = 0; x < V; ++x) {// 初始化单调栈ss = 0;tt = 1;ms[0] = 0;h[x] = g[x];for (int c = x + V, k = 1; c <= C; c += V, k += 1) {// 窗口长度最大为 Mif (k - ms[ss] > M) ss += 1;// 单调栈中,栈底元素的值最大h[c] = std::max(g[c], g[x + ms[ss] * V] + (k - ms[ss]) * W);// 压栈for (; ss < tt; --tt) {int top = ms[tt - 1];if (g[x + top * V] + (k - top) * W > g[c]) break;}ms[tt++] = k;}}}return h[C];}
混合背包
¶有
其中,第
每种物品的件数可能为 1 件([01背包][solution-01背包),无限件(完全背包)及有限件(多重背包)。
求选择将哪些物品装入背包可使获得的价值总和最大。
问题简析
¶故而,问题规约成多重背包问题。
小结
¶01背包问题、完全背包问题均可看做
多重背包问题的特例,因此混合背包问题也可以规约成多重背包问题。直接套用多重背包问题的解法即可得到复杂度为
二进制优化
空间复杂度:
时间复杂度:单调栈优化
空间复杂度:
时间复杂度:
二维费用的背包
¶有
其中,第
求选择将哪些物品装入背包可使获得的价值总和最大。
问题简析
¶虽然增加了重量一维,但是转移的状态并没有发生改变,可以完全套用 01背包的思路,只是在判断是否可以转移时要多检查一次重量是否满足条件。
记
即枚举背包的剩余容量和剩余承重值,此时第
算法优化
¶类似01背包中提到的空间优化,二维费用的背包问题也可以省略掉物品数这一维:
用一个二维数组
表示 ,则在求 时,此时 , ,若此时将 赋值给 ,则有:
小结
¶空间复杂度:
时间复杂度:
1234567891011121314151617const int MAX_C = 100 + 10;const int MAX_M = 100 + 10;int F[MAX_C][MAX_M];
int solution(int N, int C, int T) { memset(F, 0, sizeof(F));
for (int i = 0, V, W, M; i < N; ++i) { scanf("%d%d%d", &V, &M, &W); for (int c = C; c >= V; --c) { for (int m = T; m >= M; --m) { F[c][m] = std::max(F[c][m], F[c - V][m - M] + W); } } } return F[C][T];}
分组背包
¶有
其中第
所有物品被划分成
求选择将哪些物品装入背包可使获得的价值总和最大。
问题简析
¶类似01背包问题,只不过在01背包中考虑的是“要不要取第
小结
¶空间复杂度:
时间复杂度:
1234567891011121314151617181920const int MAX_C = 10000 + 10;const int MAX_N = 100 + 10;int F[MAX_C], V[MAX_N], W[MAX_N];
int solution(int N, int C) { memset(F, 0, sizeof(F));
for (int i = 0, K; i < N; ++i) { scanf("%d", &K); for (int k = 0; k < K; ++k) scanf("%d%d", V + k, W + k);
for (int c = C; c >= 0; --c) { int& ans = F[c]; for (int k = 0; k < K; ++k) { if (c >= V[k]) ans = std::max(ans, F[c - V[k]] + W[k]); } } } return F[C];}
有依赖的背包
¶有
第
物品之间具有依赖关系,比如
求选择将哪些物品装入背包可使获得的价值总和最大。
简化版的问题
¶简单起见,不妨假设所有的物品的依赖关系构成一棵树,即除根节点外的所有物品恰好依赖一个物品,且不存在循环依赖。如下图所示,
不难发现,如果要选取树上的某个节点,则其所有祖先节点都需要选取,但如果我们选取了两个节点,其公共祖先只要选取一次就可以了。反过来思考,如果我们决定要选取某个节点,则其所有的子节点都可选或可不选,此时考虑子节点的选取情况是一个 01背包问题。类似地,递归地考虑决定选取某个子节点时其子节点列表的选取情况。于是问题变成了逐层跑01背包进行状态转移的树形DP。
不妨记
转移方程为:
之后沿着树递归进行状态转移就好了。
较一般的问题
¶依然假设依赖关系中不存在环,则所有节点根据依赖关系连边构成的图是一个“森林”,问题变成了以“树”为分组的分组背包问题。而“森林”中的每棵树都可以应用简化版的问题中提到的算法来求解。
因为分组背包的时间也复杂度为
小结
¶简化版的问题
空间复杂度:
时间复杂度:有依赖的背包问题.cpp | 41 lines.1234567891011121314151617181920212223242526272829303132333435363738394041const int MAX_N = 100 + 10;const int MAX_C = 1000 + 10;int N, C, f[MAX_N][MAX_C], V[MAX_N];std::vector<int> G[MAX_N];void dfs(int u) {int* h = f[u];for (int v : G[u]) {// 计算子节点的泛化背包值dfs(v);int* g = f[v];for (int c = C; c >= V[u]; --c) {int& ans = h[c];for (int k = c - V[u]; k >= 0; --k) {ans = std::max(ans, h[c - k] + g[k]);}}}}int solution() {int root = -1;scanf("%d%d", &N, &C);for (int i = 0, v, w, p; i < N; ++i) {scanf("%d%d%d", &v, &w, &p);V[i] = v;memset(f[i], 0, sizeof(int) * (C + 1));if (v <= C) f[i][v] = w;if (p == -1)root = i;elseG[p - 1].push_back(i);}dfs(root);return f[root][C];}较一般的问题
空间复杂度:
时间复杂度:
泛化物品
¶定义
¶泛化物品指的是某类虚拟的物品,它的价值随着被分配给它的费用而变化,本质上是对于一类特殊数列或函数的概括和解释,引入它的本质是为了描述在求解背包问题是得到的中间数列,如在有依赖的背包中所所提到的,通过计算子节点而合成的描述父节点关于容量和费用的一个数列。比如,一件费用为
泛化物品的和
¶对于给定的费用
其实只需要枚举分配给其中一件物品的费用额度,将剩余的费用额度分配给另一件物品,对它们在给定额度下能获得的最大值求和,则答案是所有枚举情况下能获得价值的最大值。不妨记两件物品的费用函数为
得到的仍然是一个泛化物品的费用函数,即泛化物品的和仍为一个泛化物品,这或许可以更直观地解释有依赖的背包中合并相邻兄弟节点或两棵树的含义。
背包问题变种
¶打印方案
¶以总体积不超过背包容量的01背包问题为例。先考虑第
几个细节:
- 在检查是否可以选取物品
时需要判断背包剩余容量是否能装下它; - 因为检查时都是通过
进行判断的,因此对 要做特判;
1234567891011121314151617181920212223242526272829303132333435const int MAX_C = 1000 + 10;const int MAX_N = 1000 + 10;int F[MAX_N][MAX_C], V[MAX_N], W[MAX_N];std::vector<int> ans;
void solution(int N, int C) { ans.clear(); if (N <= 0) return;
for (int i = 0; i < N; ++i) scanf("%d%d", V + i, W + i);
// Initialize. memset(F[0], 0, std::min(C + 1, V[0]) * sizeof(int)); for (int c = V[0], w = W[0]; c <= C; ++c) F[0][c] = w;
for (int i = 1; i < N; ++i) { const int v = V[i]; const int w = W[i];
memcpy(F[i], F[i - 1], std::min(C + 1, v) * sizeof(int)); for (int c = v; c <= C; ++c) { F[i][c] = std::max(F[i - 1][c], F[i - 1][c - v] + w); } }
int c = C; for (int i = N - 1, w = F[i][C]; i > 0; --i) { if (c >= V[i] && F[i - 1][c - V[i]] + W[i] == w) { ans.push_back(i + 1); c -= V[i]; w -= W[i]; } } if (c >= V[0]) ans.push_back(1);}
最小字典序方案
¶此处的“最小字典序”指的是可行方案中的物品编号排成的序列的字典序最小。
仍以总体积不超过背包容量的01背包问题为例,在前面的分析中,我们其实得到了“最大字典序”方案,因为每次都是优先考虑编号大的物品,所以即便存在多个方案,我们也是优先取的大编号物品,因而得到的字典序是最大的。
不难想到,只要将原数组reverse一下再跑前文所述的打印方案的算法,则得到的就是“最小字典序方案”了。
1234567891011121314151617181920212223242526272829303132333435const int MAX_C = 1000 + 10;const int MAX_N = 1000 + 10;int F[MAX_N][MAX_C], V[MAX_N], W[MAX_N];std::vector<int> ans;
void solution(int N, int C) { ans.clear(); if (N <= 0) return;
for (int i = N - 1; i >= 0; --i) scanf("%d%d", V + i, W + i);
// Initialize. memset(F[0], 0, std::min(C + 1, V[0]) * sizeof(int)); for (int c = V[0], w = W[0]; c <= C; ++c) F[0][c] = w;
for (int i = 1; i < N; ++i) { const int v = V[i]; const int w = W[i];
memcpy(F[i], F[i - 1], std::min(C + 1, v) * sizeof(int)); for (int c = v; c <= C; ++c) { F[i][c] = std::max(F[i - 1][c], F[i - 1][c - v] + w); } }
int c = C; for (int i = N - 1, w = F[i][C]; i > 0; --i) { if (c >= V[i] && F[i - 1][c - V[i]] + W[i] == w) { ans.push_back(N - i); c -= V[i]; w -= W[i]; } } if (c >= V[0]) ans.push_back(N);}
方案总数
¶仍以总体积不超过背包容量的01背包问题为例,记
几个细节:
- 初始化:可能最优方案中一个物品也装不下,所以初始化时要将所有的
设置为 ,因为什么都不装也是一个方案; - 仍可以利用01背包问题中提到的滚动数组来优化空间,方法类似;
123456789101112131415const int MAX_C = 20000 + 10;int F[MAX_C];
int solution(int N, int C) { // Initialize. for (int c = 0; c <= C; ++c) F[c] = 1;
for (int i = 0, V; i < N; ++i) { scanf("%d", &V); for (int c = C; c >= V; --c) { F[c] = F[c] + F[c - V]; } } return F[C];}
最优方案的总数
¶此处的“最优方案”指物品总体积不超过背包容量且总价值最大的方案。
仍以总体积不超过背包容量的01背包问题为例,记
表示当背包剩余容量为 时,在前 件物品中做选择时可以获得的最大价值; 表示当背包剩余容量为 时,从前 件物品中进行选择可以获得最大价值的方案数;
则在转移
- 若
,说明找到了一个新方案,其在容量为 的背包中能获得的价值比之前所有的方案都要大,因此新的方案数为 ; - 若
,说明找到了一个新方案,其在容量为 的背包中能获得的价值和之前的方案一样大,因此新的方案数为 - 若
,说明未找到新方案,所以方案数仍为
几个细节:
- 初始化:可能最优方案中一个物品也装不下,所以初始化时要将所有的
设置为 ,因为什么都不装也是一个方案; - 仍可以利用01背包问题中提到的滚动数组来优化空间,只不过在转移
的状态前,要先做记录,因为需要比较 和 的值;
12345678910111213141516171819202122232425const int MAX_C = 20000 + 10;const int MOD = 1e9 + 7;int F[MAX_C], G[MAX_C];
int solution(int N, int C) { // Initialize. for (int c = 0; c <= C; ++c) { F[c] = 0; G[c] = 1; }
for (int i = 0, V, W; i < N; ++i) { scanf("%d%d", &V, &W); for (int c = C; c >= V; --c) { int total = F[c - V] + W; if (F[c] < total) { F[c] = total; G[c] = G[c - V]; } else if (F[c] == total) { G[c] = (G[c] + G[c - V]) % MOD; } } } return G[C];}
求第
- 待补充。
Related
¶