目录
2218. 从栈中取出 K 个硬币的最大面值和 - 力扣(LeetCode)
前置知识
前置知识请看另一篇博文 动态规划基础讲义。
引入
在正是讲解各类背包问题时,我们先来看看一道题目。
P2871 [USACO 07 DEC] Charm Bracelet
提议概要:
在上述例题中,由于每个物体只有两种可能的状态(取与不取),对应二进制中的 0 和 1 ,这类问题便被称为「0-1 背包问题」。
0 - 1 背包问题
解释
已知我们有 W 容量的背包, 以及 价值和
存储体积的 n 个物品。同时,每一件物品只能选取一次,试问如何选取物品可以使背包内物品价值最大?
就如下图所示:
解题思路
例题中已知条件有第 i 个物品的重量 ,价值
,以及背包的总容量 W 。
设 DP 状态 f(i, j) 为在只能放前 i 个物品的情况下,容量不超过 j 的背包所能达到的最大总价值。
考虑状态转移方程,假设我们当前处理到第 i 个物品,即前 i - 1 个物品已经处理完毕。根据 [0-1背包问题] 的阐述,我们对物品 i 只有两种处理(策略)方式----选取(1)/不选取(0)。所以,我们可以写出两种处理方式下对应的方程:选取 --> f(i, j) = f(i - 1, j - ) +
, 不选取 --> f(i, j) = f(i - 1, j)。
此外,关注到 [0-1背包问题] 是一个最大化问题。所以,f(i, j) = max{选取情况, 不选取情况}。
由此可以得出状态转移方程:
f(i, j) = max{f(i - 1, j - ) +
, f(i - 1, j)}
务必牢记并理解这个转移方程,因为大部分背包问题的转移方程都是在此基础上推导出来的。
因此,我们获取得到关键代码段
for (int i = 1; i < n; ++i) {//处理到第 i 个物品
for (int l = w[i]; l < W; ++l) {//枚举重量
f[i][l] = max(f[i - 1][l], f[i - 1][l - w[i]] + v[i]);//根据我们推导出的公式编写
}
}
空间优化
我们知道外部循环固定了我们处理到第 i 个物品。所以我们可以将 f(i, j) --> f(j)。但是,此时就需要注意一些设计细节。
j - w[i] | ... | j | |
i - 1行 | 1 | ... | 1 |
i 行 | 0 | ... | 2 |
注:1表示计算时需要的状态, 2表示计算的状态。
而现在我们需要去除行的信息,那么上表变形为
j - w[i] | ... | j | |
i - 1行/ i 行 | 1 | ... | 1/2 |
注:1表示计算时需要的状态, 2表示计算的状态,蓝色表示i - 1行,红表示 i 行。
我们发现为了保证能够正确的转移,所以次数数组的前部分是第 i - 1行的信息即蓝色,后部分是第 i 行的信息即红色。
所以关键代码修改为
for (int i = 1; i < n; ++i) {//处理到第 i 个物品
for (int l = W; l > w[i]; --l) {//枚举重量
f[l] = max(f[l], f[l - w[i]] + v[i]);
}
}
引例AC代码
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 13010;
int n, W, w[maxn], v[maxn], f[maxn];
int main() {
cin >> n >> W;
for (int i = 1; i <= n; i++) cin >> w[i] >> v[i]; // 读入数据
for (int i = 1; i <= n; i++) {
for (int l = W; l >= w[i]; l--) {
f[l] = std::max(f[l],f[l - w[i]] + v[i]); // 状态方程
}
}
cout << f[W];
return 0;
}
本节思考
1.如果题目被改写为每个物品可以放置无数件呢?
也就是 P1616 疯狂的采药
2.如果我们增加一个新的费用维度呢?
3.如果我们给物品加上组别,每一组内只能选取一个物品,如何选取物品才能获取最大价值。
4.如果我们选取某个物品的具有附加条件,即选择该物品前必须选择另外一个物品。如何获取最大值?
完全背包问题
由 [0-1背包问题] 最后的思考我们便引出了完全背包问题。
解释与解析
已知我们有 W 容量的背包, 以及 价值和
存储体积的 n 个物品。同时,每一件物品可以选取无数次,试问如何选取物品可以使背包内物品价值最大?
OKOK,我当然知道上面的表述是有问题的!在背
包有容量限制的条件下,一个物品可以选取无数次是毫无意义的!所以我本人更喜欢表述为每一件物品可以选取足够多次。什么意思呢?也就是我们的第 i 件物品至多只能选取 , 其中符号
表示向下取整。
所以我们不妨像 [0-1背包问题] 那样来假设处理。f(i, j) 表示处理到第 i 个物品,不超过重量 j 的最大价值。此时,我们假设我们已经处理到了第 i 个物品,即前 i - 1个物品处理完毕。面对完全背包问题,我们对物品 i 的处理(策略)方式只有两个----不选取/选取,选1到 件。所以,我们可以给出对应的状态方程。
不选取:f(i, j) = f(i - 1, j)
选取:f(i, j) = f(i - 1, j - k * w[i]) + k * v[i], 其中 。
根据上述表达式,我们发现我们需要对 k 进行枚举。但是,面对此类递推式子我们是可以优化的。为了表达 f(i, j), 我们目光来到表达式 f(i, j - w[i]) 身上。
根据上述表达式子,我们知道 f(i, j - w[i])如下
不选取:f(i, j - w[i]) = f(i - 1, j - w[i])
选取:f(i, j - w[i]) = f(i - 1, j - w[i] - k * w[i]) + k * v[i], 其中 。
因此 f(i, j) 在选取的情况下,被 f(i, j - w[i]) 完美包含。
由此我们得到递推表达式子:
f(i, j) = max{f(i, j - ) +
, f(i - 1, j)}
同样的,我们对完全背包进行空间优化 f(i, j) --> f(j)。
j - w[i] | ... | j | |
i - 1行 | 0 | ... | 1 |
i 行 | 1 | ... | 2 |
注:1表示计算时需要的状态, 2表示计算的状态。
而现在我们需要去除行的信息,那么上表变形为
j - w[i] | ... | j | |
i - 1行/ i 行 | 1 | ... | 1/2 |
注:1表示计算时需要的状态, 2表示计算的状态,蓝色表示i - 1行,红表示 i 行。
同 [0-1背包问题] 的分析,所以我们的循环方向应该是正向的。
for (int i = 1; i < n; ++i) {//处理到第 i 个物品
for (int l = w[i]; l <= W ; ++l) {//枚举重量
f[l] = max(f[l], f[l - w[i]] + v[i]);
}
}
PS:这里能够优化的前提是 能取到恰当大。
0-1背包思考题AC代码
#include <iostream>
using namespace std;
const int maxn = 1e4 + 5;
const int maxW = 1e7 + 5;
int n, W, w[maxn], v[maxn];
long long f[maxW];
int main() {
cin >> W >> n;
for (int i = 1; i <= n; i++) cin >> w[i] >> v[i];
for (int i = 1; i <= n; i++) {
for (int l = w[i]; l <= W; l++) {
if (f[l - w[i]] + v[i] > f[l]) f[l] = f[l - w[i]] + v[i]; // 核心状态方程
}
}
cout << f[W];
return 0;
}
本节思考
如果 不能取到恰当大呢?也就是我们做出限制,没见物品至多选取
个。
多重背包问题
啊哈!没想到吧,由 [完全背包问题] 的思考,或者说对其做出的限制便引出了我们新一节的主角!
没错,在 [完全背包问题] 的基础上对 做出新限制,便成为了我们多重背包。
所以,希望读者能够明白各个背包之间的关系与区别,从而可以合理运用、快速编码。基于目前为止的背包问题表述大体相同,而区别在于思考之处。所以在此我们就不给出解释了。
解析
还记得吗?我们曾在完全背包问题中便指出需要对 进行枚举(已在上节,红字标出)!所以有一个很自然的代码段落,也许这也是网上最常见到代码段落。
for (int i = 1; i < n; ++i) { //处理到第 i 个物品
int max_k = min(k[i], W / w[i]);
for (int times = 0; times <= max_k; ++times) {
for (int l = times * w[i]; l <= W ; ++l) { //枚举重量
f[i][l] = max(f[i][l], f[i - 1][l - times * w[i]] + times * v[i]);
}
}
}
同样的,我们先来思考能否进行空间优化。同 [0-1背包问题] 和 [完全背包问题] 一样,我们可以打表或者画图,来思考。这部分留个读者自己动手操作、思考。我们直接给出答案。
for (int pos = 1; pos < n; ++pos) {
int K = min(W / w[i], k[i]); //最大选取次数
int min_w = w[i]; //最小选取额度
for (int l = W; l >= min_w; --l) {
for (int k = 1; k <= K; ++k) {
if (l - k * min_w < 0) break;//当前重量不支持选取k件物品
f[l] = max(f[l], f[l - k * min_w] + k * v[i]);
}
}
}
现在我们给出时间复杂度 O()。
二进制分组优化
之所以将该优化单独拉出来,是因为该优化的底层思想比较重要。同时,在网络上众多教程所提及而不讲解的。
之所以提出这个优化方式,是基于一个想法--将 [多重背包问题] 转置为 [0-1背包问题],是基于一个思想--整体/局部思想,是基于一个底层计算机逻辑--二进制数。
解释
显然的是,我们知道 O(N * W)的部分是不能够优化了。可以说该时间复杂度是背包问题的下限,因为所有背包问题在一定程度上,都可以转置为 [0-1背包问题] 而 [0-1背包问题] 的时间解决下限便是 O(N * W)。所以我们只能从 下手。
为了表述方便,我们用 A(i,j) 代表第 i 种物品拆分出的第 j 个物品。
在朴素的做法中, 对于 ,A(i, j) 均表示相同物品。那么我们效率低的原因主要在于我们进行了大量重复性的工作。举例来说,我们考虑了「同时选 A(i, 1) 和 A(i, 2)」与「同时选 A(i, 2) 和 A(i, 3)」这两个完全等效的情况,因为这种情形都是选取了两个第 i 种物品。这样的重复性工作我们进行了许多次。那么优化拆分方式就成为了解决问题的突破口。
过程
我们可以通过「二进制分组」的方式使拆分方式更加优美。
具体地说就是令 A(i, j), j ∈ (0, ) 分别表示由
个单个物品「捆绑」而成的大物品(整体思想)。特殊地,若
不是 2 的整数次幂,则需要在最后添加一个由
个单个物品「捆绑」而成的大物品用于补足(也就是,剩余部分单独捆绑)。
举几个例子:
6 = 1 + 2 + 3;
8 = 1 + 2 + 4 + 1;
18 = 1 + 2 + 4 + 8 + 3;
31 = 1 + 2 + 4 + 8 + 16;
显然,通过上述拆分方式,可以表示任意 个物品的等效选择方式。将每种物品按照上述方式拆分后,使用 0-1 背包的方法解决即可。
时间复杂度 O()
代码实现
index = 0;
for (int i = 1; i <= n; i++) {
int c = 1, ith_w, ith_v, k;
cin >> ith_w >> ith_v >> k; //获取第 i 种物品重量、价值、最大选取个数
//捆绑操作
while (k > c) {
k -= c;
list[++index].w = c * ith_w;
list[index].v = c * ith_v;
c *= 2;
}
//剩余部分捆绑
list[++index].w = ith_w * k;
list[index].v = ith_v * k;
}
完整实现代码
int main() {
//读入 N, W
cin >> N >> W;
int index = 0;
for (int i = 1; i <= N; i++) {
int c = 1, ith_w, ith_v, k;
cin >> ith_v >> ith_w >> k; //获取第 i 种物品重量、价值、最大选取个数
//捆绑操作
while (k > c) {
k -= c;
list[++index].w = c * ith_w;
list[index].v = c * ith_v;
c *= 2;
}
//剩余部分捆绑
list[++index].w = ith_w * k;
list[index].v = ith_v * k;
}
N = index;
//0-1背包代码
for (int i = 0; i <= N; ++i) {
for (int l = W; l >= list[i].w; --l) {
f[l] = max(f[l], f[l - list[i].w] + list[i].v);
}
}
cout << f[W];
return 0;
}
小试牛刀
根据上述模板,可以获得代码
#include <iostream>
#include <algorithm>
using namespace std;
int N, W;
struct Product {
int w, v;
};
Product list[1000001];
int f[400005];
int main() {
//读入 N, W
cin >> N >> W;
int index = 0;
for (int i = 1; i <= N; i++) {
int c = 1, ith_w, ith_v, k;
cin >> ith_v >> ith_w >> k; //获取第 i 种物品重量、价值、最大选取个数
//捆绑操作
while (k > c) {
k -= c;
list[++index].w = c * ith_w;
list[index].v = c * ith_v;
c *= 2;
}
//剩余部分捆绑
list[++index].w = ith_w * k;
list[index].v = ith_v * k;
}
N = index;
//0-1背包代码
for (int i = 0; i <= N; ++i) {
for (int l = W; l >= list[i].w; --l) {
f[l] = max(f[l], f[l - list[i].w] + list[i].v);
}
}
cout << f[W];
return 0;
}
单调队列优化
单调队列的优化关键在于对表达式子的变形。任然我们不能够对 O(NW) 的部分进行优化,所以目标任然是 。
考虑优化 f(i, j) 的转移。为方便表述,设 g(x, y) = f(i, x * w[i] + y), g'(x, y) = f(i - 1, x * w[i-1] + y)则转移方程可以表示为:
注意下面的操作, 我愿称为 “神来一手”。设 G(x, y) = g'(x, y) - x * v[i]。
那么 g'(x - k, y) + v[i] * k = g'(x - k, y) - (x - k) *v[i] + k * v[i] + x * v[i] = G(x - k, y) + x * v[i]。
这里使用了“凑”的变换技巧,因为造成我们麻烦的是k,所以要消去k,就要产生 + k * v[i] 的项,使用常值或者我们容易获取的值来代替 k。这是变换的初衷。
状态转移方程又可以进一步表述为
做一些解释,这里 y 又可以称为剩余量,如果你看到其他blog。其实这里使用了整数的余数表示方式,即 整数 = 倍数 * 因子 + 余数。为什么想到?很简单因为我们每次 - w[i],在数学的表达式上就是 j = x * w[i] + y。
OK,扯回正题。根据上述表达式,我们可以反应过来可以使用单调栈/单调队列来维护。
代码实现
版本1:
int solve(int N, int W, vector<int>& w, vector<int>& v, vector<int>* k) {
vector<int> dp(W + 1, 0); // g
vector<int> pre; // 辅助数列g',保存上一次dp(数组)值。见推理过程
vector<int> que(W + 1); // 当前队列,
for (int i = 0; i < N; ++i) {//循环初始条件按照实际情况来,i表示第i种物品
pre = dp;
for (int y = 0; y < w[i]; ++y) {//枚举剩余数-->正确、不漏地表示每一个数
int head = 0; int tail = -1;//单调队列的首尾指针,初始--空队列
for (int num = y; num <= W; num += w[i]) {
// 剔除不满足关系的队头,对头是最大值
// 那满的重量都不足 num
while (head <= tail && num >= que[head] + w[i] * k[i]) ++head;
// 使用G的等价表达式剔除尾部小于此时新G的部分
while (head <= tail &&
pre[num] - (num - y)/w[i] * v[i] >= pre[que[tail]] - (que[tail] - y)/w[i] *v[i]) --tail;
que[++tail] = num;
dp[num] = pre[que[head]] + (num - que[head])/w[i] * v[i];
}
}
}
return dp[W];
}
上述的编码方式更容易理解一点。当然有也以下版本。
版本2:
int solve(int N, int W, vector<int>& w, vector<int>& v, vector<int>* k) {
vector<int> dp(W + 1, 0); // g
vector<int> pre; // 辅助数列g',保存上一次dp(数组)值。见推理过程
vector<int> que(W + 1); // 当前队列,
for (int i = 0; i < N; ++i) {//循环初始条件按照实际情况来,i表示第i种物品
pre = dp;
for (int y = 0; y < w[i]; ++y) {//枚举剩余数-->正确、不漏地表示每一个数
int head = 0; int tail = -1;//单调队列的首尾指针,初始--空队列
for (int x = 0; x * w[i] + y <= W; ++x) {
// 剔除不满足关系的队头,对头是最大值
// 拿满都不足 x 个
while (head <= tail && x >= que[head] + k[i]) ++head;
// 使用G的等价表达式剔除尾部小于此时新G的部分
while (head <= tail &&
pre[y + x*w[i]] - x * v[i] >= pre[y + que[tail]*w[i]] - que[tail]*v[i]) --tail;
que[++tail] = x;
//使用g 和 g'的关系更新
dp[y + x*w[i]] = pre[y + que[head]*w[i]] + (x - que[head]) * v[i];
}
}
}
return dp[W];
}
时间复杂度O(NW)。
小试牛刀
根据代码思路,我们得到AC代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
//多重背包问题: 限制每种物品可取次数
//究极优化:单调队列
const int M = 20010, N = 1010;
int n, m;
int dp[M], g[M];
int que[M]; //队列只存储在同余的集合中是第几个,不存储对应值
int main() {
cin >> n >> m;
for(int i = 0; i < n; i ++){
int v, w, s;
cin >> v >> w >> s;
//复制一份副本g,因为这里会是从小到大,不能像0-1背包那样从大到小,所以必须申请副本存i-1状态的,不然会被影响
memcpy(g, dp, sizeof dp);
for(int r = 0; r < v; r ++) { //因为只有与v同余的状态 相互之间才会影响,余0,1,...,v-1 分为v组
int head = 0, tail = -1;
for(int k = r; k <= m; k += v) { //每一组都进行处理,就相当于对所有状态都处理了
if(head <= tail && k > que[head] + s*v) head++;
while(head <= tail && g[k] - (k - r)/v * w >= g[que[tail]] - (que[tail] - r)/v * w) tail --;
que[++ tail] = k;
dp[k] = g[que[head]] + (k - que[head])/v * w;
}
}
}
cout << dp[m] << endl;
return 0;
}
同时,我们给出 [luogu] P1776 宝物筛选 的此种解法代码
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
//多重背包问题: 限制每种物品可取次数
//究极优化:单调队列
const int M = 40005, N = 105;
int n, m;
int dp[M], g[M];
int que[M]; //队列只存储在同余的集合中是第几个,不存储对应值
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
int v, w, s;
cin >> w >> v >> s;
//复制一份副本g,因为这里会是从小到大,不能像0-1背包那样从大到小,所以必须申请副本存i-1状态的,不然会被影响
memcpy(g, dp, sizeof dp);
for (int r = 0; r < v; r++) { //因为只有与v同余的状态 相互之间才会影响,余0,1,...,v-1 分为v组
int head = 0, tail = -1;
for (int k = r; k <= m; k += v) { //每一组都进行处理,就相当于对所有状态都处理了
if (head <= tail && k > que[head] + s * v) head++;
while (head <= tail && g[k] - (k - r) / v * w >= g[que[tail]] - (que[tail] - r) / v * w) tail--;
que[++tail] = k;
dp[k] = g[que[head]] + (k - que[head]) / v * w;
}
}
}
cout << dp[m] << endl;
return 0;
}
二维费用背包
还记得我们的在 [0-1背包问题] 中的第2个思考吗?第2个思考:如果增加一个新的费用维度。没错增加一个新的的费用维度,题目变成了二位费用背包。N维费用背包大家也可以自己理解了。稍微提一嘴,其实你可以认为在此之前的背包问题都视为一维费用背包问题。
所以这个是很明显的 0-1 背包问题,可是不同的是选一个物品会消耗两种价值(以榨取为例子是经费、时间),只需在状态中增加一维存放第二种价值即可。
这时候就要注意,再开一维存放物品编号就不合适了,因为容易 MLE。
二维费用背包关键代码为
for (int k = 1; k <= n; k++)
for (int i = m; i >= mi; i--) // 对经费进行一层枚举
for (int j = t; j >= ti; j--) // 对时间进行一层枚举
dp[i][j] = max(dp[i][j], dp[i - mi][j - ti] + 1);
[luogu] P1855 榨取kkksc03 的AC代码为
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,M,T,dp[1010][1010];
int m,t;
int main()
{
scanf("%d%d%d",&n,&M,&T);
for(int i=1;i<=n;i++)
{
//仅仅只是多了一维而已
scanf("%d%d",&m,&t);
for(int j=M;j>=m;j--)
for(int k=T;k>=t;k--)
{
dp[j][k]=max(dp[j][k],dp[j-m][k-t]+1);
}
}
printf("%d\n",dp[M][T]);
}
分组背包
OK,我们通过最基本的 [0-1背包问题] 与 [完全背包问题] 的思考加深,得到了许多新的背包问题以及他们的解决方案。现在我们需要面对 [0-1背包问题] 的第三个思考问题:如果给物品加上组别,组别内的物品是相互冲突的,即不能存放在一起。而组间的物品是可以存放在一起的。请问此时如何选取物品使得背包内价值最大。
基于 [0-1背包问题] 提出的第三个思考问题,我们衍生出了分组背包问题。借此我们正是进入到分组背包的学习。
解析
通过我们之前的问题学习与锤炼,我们可以很快反应出来需要对 [0-1背包问题] 式子做出恰当变形。
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设 F [k , v] F[k, v]F[k,v] 表示前 k
组物品花费费用 v
能取得的最大权值(收益),则有:
我们在此给出伪码
for (group k = 1; k < N; ++k) {
for (tar = 0; tar <= target; ++tar) {
for (item i = 1; i < size(k); ++i) {
//状态转移方程
}
}
}
小试牛刀
Leetcode 1155. 掷骰子等于目标和的方法数
AC代码
class Solution {
public:
int numRollsToTarget(int n, int k, int target) {
vector<vector<int>> dp = vector<vector<int>>(n + 1, vector<int>(target + 1, 0));
dp[0][0] = 1;
int mod = 1000000007;
for(int i = 1; i <= n; ++i){
for(int tar = 1; tar <= target; ++tar){
for(int j = min(k, tar); j > 0; --j){
dp[i][tar] = (dp[i-1][tar - j] + dp[i][tar]) % mod;
}
}
}
return dp[n][target];
}
};
2218. 从栈中取出 K 个硬币的最大面值和 - 力扣(LeetCode)
提示:
此题需要做一些变换,因为一个栈中的元素是非互斥的。因此,不能将 “栈” 视作一个分组。因此,需要做出变换。我们发现最大情况是固定的、可知的,即某一个 “栈” 取出几个硬币是固定的。而某一个栈取多少硬币是互斥的。
PS:例如最大价值的情况是第一个 “栈” 取 2 枚硬币,那么最大情况不可能是第一个 “栈” 取其他次数的硬币。所以说某一个栈取多少硬币是互斥的。
AC代码
class Solution {
public:
int maxValueOfCoins(vector<vector<int>>& piles, int k) {
int n = piles.size();//n个栈;
vector<int> dp = vector<int>(k + 1, 0);
for(int i = 0; i < n; ++i){
for(int j = 1; j < piles[i].size(); ++j){
piles[i][j] += piles[i][j - 1];
}
}//一个栈分成n组,使用前缀和维护。
for(int i = 0; i < n; ++i){
for(int times = k; times >= 0; --times){
for(int j = 1; j <= piles[i].size(); ++j){
if(times >= j) dp[times] = std::max(dp[times], dp[times - j] + piles[i][j - 1]);
}
}
}
return dp[k];
}
};
总结
我相信大家发现了,在对于第一道题和第二题,我们的DP数组维度是不同的。首先,这里有一些细节。例如,“掷骰子” 这道题是每轮都掷骰子的,也就是每组都要选取一个元素;而 “硬币和” 则是没有每组都要取一个元素的情况。所以,“掷骰子” 没有去掉 组别的维度。
有依赖背包
(长叹一口气...)
终于,来到最后一个背包问题了。这个背包问题对应就是 [0-1背包问题] 中的思考4。而这个问题已经半只脚踏入了 树形DP问题。这是有难度,请耐心沉淀。
一般我们研究的都是简化的问题版本,也就是有依赖背包问题还有一个前提条件:一个物品能被多个物品依赖,但是不能依赖多个物品。
那么我们可以使用简单的数据结构--树来反应上述关系,如下图。
我们拥有一个 “主件(host)” 和 k 个 “附件(Appendixes)”。现在,摆在我们面前的问题是每一个附件都有选与不选两种状态。所以,如果枚举所有选择状态那么我们的时间复杂度在 O()。这所付出的代价是高昂的。
可是,我们的策略并不是死路一条。因为我们需要获取的是最大价值。所以从另外一个角度出发,我们即便是 相同费用 的 n 个方案,我们也是调取价值最大的方案。
而记录 相同费用方案中的最大值恰巧就是 [0-1背包问题]。当然这里的费用是一维的(此时费用为重量)。也就是说,我先对 “附件” 进行一次 [0-1背包]。
但是上面的图只是我们的简单情况,可能附件还有附件。所以要对附件的附件进行一次 [0-1背包],而反应到上层 k个附件已然变成了 k 个分组。所以我们对每一个 “主件” 进行一次 [分组背包]
实现代码:
//考虑从 主件(根) u 处理
void dfs(int u) {
for (int appendix: appendixes) {
dfs(appendix)
//进行一次分组背包,注意前提选附件必要选主件 W - w[u]
for (int m = W - w[u]; m >= w[appendix]; --m) {
for (int k = 0; k <= m; ++k) {
f[u][m] = max(f[u][m], f[u][m - k] + f[appendix][k]);
}
}
}
//加入 主件u 的值
for (int m = W; m >= w[u]; --m) f[u][m] = f[u][m - w[i]] + v[u];//能选状态
for (int m = 0; m < w[u]; ++m) f[u][m] = 0;//不能选状态
return ;
}