文章目录
0. 前言
背包问题是众多 dp
问题的母题,是一个很重要的知识点。该博文基于背包九讲总结,会将背包九讲内容及模板题全部总结一般,也是鉴于学习进度,目前仅总结了 01 背包及优化模型,完全背包,多重背包,分组背包。
初次系统学习背包问题,总结不够到位,往各位同学批评指正!十分感谢~
背包问题共性:
n
个物品,一个容量为v
的背包- 每个物品两个属性,体积
vi
,价值wi
- 要求在这些物品中挑总体积不大于
v
的物品并使背包装入物品的总价值w
最大。不一定需要装满背包
1. 01背包
特点:
- 每件物品最多只用一次
朴素01背包
思路:
- 状态表示:
f[i][j]
从前i
个物品中选择且总体积不大于j
的最大价值 - 状态计算:
- 将整个状态划分成两类:
- 不选第
i
个物品:f[i][j] = f[i-1][j]
- 选第
i
个物品:f[i][j] = f[i-1][j-v[i]]+w[i]
- 假设前
i-1
个物品的总体积为v
,现在加上v[i]
后小于等于总体积j
,即v+v[i]<=j
,则有v <= j - v[i]
,就以j >= v[i]
作为判断条件
- 假设前
- 故状态转移方程为:
f[i][j]=max(f[i-1][j], f[i-1][j-v[i]]+w[i])
- 状态初始化:
f[0][0~m] = 0
表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
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 = 0; j <= m; ++j) {
f[i][j] = f[i - 1][j]; // 不选第i件物品的情况一定存在
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
如上分析过程及代码即为最朴素的 01 背包问题的解决方案。
滚动数组优化
- 能发现,
f[i][j]
的状态转移仅使用到了f[i-1][...]
,故可以采用滚动数组来做。即当前层的状态转移仅与上一层有关 - 当前层是
i & 1
,上一层是i-1 & 1
滚动数组优化代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int n, m;
int v[N], w[N];
int f[2][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 = 0; j <= m; ++j) {
f[i & 1][j] = f[i - 1 & 1][j];
if (j >= v[i]) f[i & 1][j] = max(f[i - 1 & 1][j], f[i - 1 & 1][j - v[i]] + w[i]);
}
}
cout << f[n & 1][m] << endl;
return 0;
}
一维终极优化
其实,从状态转移方程 f[i][j]=max(f[i-1][j], f[i-1][j-v[i]]+w[i])
能够知道第一维 f[i]
只用到了 f[i-1]
,且第二维不论是 f[i-1][j]
还是 f[i-1][j-v[i]]
第二维总是小于等于 j
的。基于滚动数组的思想,我们完全可以将其改为一维数组来再度优化空间和代码:
- 基于最朴素二维数组版本,我们可以直接删掉第一维,则
f[N][N]
变为f[N]
,仅枚举体积 - 则朴素代码中的
f[i][j]=f[i-1][j]
变为f[j]=f[j]
成为恒等式,则可以直接删除 if
判断中j >= v[i]
,此时仅有一维,当j < v[i]
时,这个判断条件是没有意义的。故我们可以直接让j
从v[i]
开始,就可以删掉if
判断了- 此时,如果直接删掉第一维,则变为:
f[j]=max(f[j],f[j-v[i]]+w[i])
,这个转移方程其实和之前是不等价的。可将其在一维含义下还原成两维的含义,对比在优化过程中是否改变了原意。- 首先第
i
层算的f[i][j] = max(f[i][j],...)
,第i
层算的f[j]
一定是f[i][j]
,但是由于一维中是f[j-v[i]]
,j-v[i]
一定是严格小于j
的,且我们的j
是从小到大进行枚举体积的,j-v[i]
会在j
之前被计算,那么一维中的f[j-v[i]]
实际上是第i
层的j-v[i]
,其等价于f[i][j-v[i]
,而实际上应该是f[i-1][j-v[i]]
- 故我们可以改变
j
的循环顺序来解决这个问题,让j
从m
到v[i]
进行枚举,即从大到小枚举体积,那么当我们更新体积j
时,这个j-v[i]
还没被更新过,那么它就存的是i-1
层的j-v[i]
这样就等价于之前的状态转移方程了
- 首先第
- 至此,我们就完成了 01 背包问题的终极写法
一维 01 背包终极写法代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int n, m;
int v[N], w[N];
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;
}
2. 完全背包
特点:
- 每件物品可以使用无限次
朴素完全背包
思路:
- 状态表示:
f[i][j]
从前i
个物品中选择且总体积不大于j
的最大价值 - 状态计算:
- 将整个状态划分成
k
类: - 选第
i
个物品 0 次:f[i][j] = f[i-1][j]
- 选第
i
个物品 1 次:f[i][j] = f[i-1][j-v[i]]+w[i]
- 选第
i
个物品 2 次:f[i][j] = f[i-1][j-2*v[i]]+2*w[i]
- 选第
i
个物品 k 次:f[i][j] = f[i-1][j-k*v[i]]+k*w[i]
- 故状态转移方程为:
f[i][j]=max(f[i][j], f[i-1][j-k*v[i]]+k*w[i]))
- 将整个状态划分成
- 状态初始化:
f[0][0~m] = 0
表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0
这个时间复杂度是相当高的,是 O ( n 3 ) O(n^3) O(n3),当 n = 1000 n = 1000 n=1000 时,计算量达到 1 e 9 1e9 1e9,妥妥的超时。不过这也是朴素做法的基本思想。
朴素思想的超时代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
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 = 0; j <= m; ++j) {
for (int k = 0; k * v[i] <= j; ++k) {
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
状态转移方程优化
简单展开状态转移方程:
f[i][j] =max(f[i-1][j], f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w,...)
f[i][j-v] =max( f[i-1][j-v], f[i-1][j-2v]+w, f[i-1][j-3v]+2w, f[i-1][j-4v]+3w,...)
f[i][j-v]+w=max( f[i-1][j-v]+w, f[i-1][j-2v]+2w, f[i-1][j-3v]+3w, f[i-1][j-4v]+4w,...)
故:
f[i][j] =max(f[i-1][j], f[i][j-v]+w);
这是一个经典的优化,可以优化掉一维的状态,时间复杂度优化为 O ( n 2 ) O(n^2) O(n2)。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
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 = 0; j <= m; ++j) {
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
对比下完全背包与 01 背包的转移方程:
- 01 背包:
f[i][j]=max(f[i-1][j], f[i-1][j-v]+w)
- 完全背包:
f[i][j]=max(f[i-1][j], f[i][j-v]+w)
01 背包从 i-1
转移过来,完全背包从 i
转移过来,就这一点不同。那么在此枚举体积就不需要从大到小枚举了,因为当前是 f[i][[j] = f[i][j-v]+w
就是从当前第 i
层转移过来的,被提前计算的 f[i][j-v]
刚好帮助了状态转移。
在此简单总结:除了完全背包问题一维优化后,其体积是从小到大循环的。其余大部分背包问题一维优化后都是从大到小循环的。在此包括 01 背包,分组背包、多重未优化、混合、有依赖等等。其中多重背包问题写法很多,单调队列版本从小到大循环 如果空间是两维的话,空间随便循环,循环顺序也都是随意。且优化后顺序为 物品、体积、决策
那么完全背包也完全可以优化成一维:
一维终极优化
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int n, m;
int v[N], w[N];
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 = v[i]; j <= m; ++j) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
这里的优化就不需要从大到小枚举 j
了,这里的 f[j-v[i]]
就是需要第 i
层的 j-v[i]
,在当前 i
层,j-v[i]
一定是先于 j
被更新的,满足要求。
3. 多重背包
特点:
- 每件物品有独立的个数限制,不得超过最大数量限制
思路:
- 状态表示:
f[i][j]
从前i
个物品中选择且总体积不大于j
的最大价值 - 状态计算:
- 将整个状态划分成
s[i]+1
类: - 选第
i
个物品 0 次:f[i][j] = f[i-1][j]
- 选第
i
个物品 1 次:f[i][j] = f[i-1][j-v[i]]+w[i]
- 选第
i
个物品 2 次:f[i][j] = f[i-1][j-2*v[i]]+2*w[i]
- 选第
i
个物品s[i]
次:f[i][j] = f[i-1][j-s[i]*v[i]]+s[i]*w[i]
最终就是该物品数量的最大限制。十分类似于完全背包问题 - 故状态转移方程为:
f[i][j]=max(f[i][j], f[i-1][j-k*v[i]]+k*w[i])), k=0, 1, 2,...
- 将整个状态划分成
- 状态初始化:
f[0][0~m] = 0
表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0
朴素版本的多重背包问题与朴素版本的完全背包问题思想一模一样,代码稍作改动即可。时间复杂度仍为 O ( n 3 ) O(n^3) O(n3),这里的 n = 100 n=100 n=100,故计算次数为 1 e 6 1e6 1e6,还是可以的。
朴素多重背包
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 105;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= m; ++j)
for (int k = 0; k * v[i] <= j && k <= s[i]; ++k)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
仍为多重背包问题,但数据范围增大,仍采用
O
(
n
3
)
O(n^3)
O(n3) 暴力则将达到
1000
∗
2000
∗
2000
=
4
e
9
1000 * 2000 * 2000 = 4e9
1000∗2000∗2000=4e9 40 亿的计算量,超时。
考虑展开状态转移方程,以完全背包问题优化方式进行优化:
f[i][j] =max(f[i-1][j], f[i-1][j-v]+w, f[i-1][j-2v]+2w, ... ,f[i-1][j-sv]+sw)
f[i][j-v] =max( f[i-1][j-v], f[i-1][j-2v]+w, ... ,f[i-1][j-sv]+(s-1)w, f[i-1][j-(s+1)v]+sw)
乍一看还挺工整,f[i][j-v]
和 f[i][j]
后面一部分是比较相似的,但是其中 f[i][j-v]
多了 f[i-1][j-(s-1)v]+sw
这一项,即现在我们已知 f[i][j-v]
的最大值,需要求解 f[i][j-v]
展开项中除过 f[i-1][j-(s-1)v]+sw
的最大值。这个操作是无法实现的,取最大值操作是不支持减法的。这就相当于给定一堆数的最大值,让你求解其中部分数的最大值,这是无法直接求得的。所以,我们无法直接使用完全背包优化方式来优化多重背包问题。
二进制优化
在此有一种神奇且经典的优化方式,称为:二进制优化方式
- 假设某组物品有 1023 个,那么我们真的需要枚举 1023 次吗?
- 这里可以采用二进制倍增的思想,将 1023 个物品进行打包,然后拼凑出 1~1023 中的任意数量的物品
- 思想类比快速幂,将 O ( n ) O(n) O(n) 优化到 O ( l o g n ) O(logn) O(logn)
- 即,若第
i
个物品数量为s
个,优化流程为:- 将
s
拆分成二进制下的打包物品,s[i]
个就会变成log s[i]
个 - 然后对打包出来的物品做一遍 01 背包就行了,每个打包物品只能选 1 次。因为这些打包物品可以拼凑出来所有的情况
- 将
那么原来的算法时间复杂度为 O ( n v s ) O(nvs) O(nvs),现在为 O ( n v l o g s ) O(nvlogs) O(nvlogs)。成功优化, 1000 ∗ 2000 ∗ l o g 2000 = 2 e 7 1000*2000*log2000=2e7 1000∗2000∗log2000=2e7,刚好能过。
这就是多重背包问题的经典优化:二进制优化。
二进制优化思想代码:
#include <iostream>
#include <algorithm>
using namespace std;
// 一共1000个物品,每个物品最多2000件,2^11=2048 数组大小开1000*11=11000
const int N = 11005, M = 2005;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; ++i) {
int a, b, s; // 当前物品的 体积 价值 个数
cin >> a >> b >> s;
int k = 1; // 从 1 开始打包
while (k <= s) {
cnt ++; // 记录新打包物品编号,每次打包k个第i个物品
v[cnt] = a * k; // k个一组,体积变大k倍
w[cnt] = b * k; // k个一组,价值变大k倍
s -= k; // i物品总个数一次性减少k个
k *= 2; // 倍增
}
if (s > 0) { // 补上剩下的物品
cnt ++;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt; // 更新现在有的物品总数,将其转化为01背包问题,每个物品独立且只能选1次
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。本题属于单调队列优化,将多重背包问题做到了
O
(
n
m
)
O(nm)
O(nm) 的时间复杂度。因为每个元素在单调队列中只进队一次出队一次所有是 m
次,外层循环枚举 n
次,所以总的时间复杂度是
O
(
n
m
)
O(nm)
O(nm) 的。是一个了不起的优化!
我能理解该问题,板书很潦草很潦草…推荐几个 dalao
题解,他们写的很清楚。
感觉写了一堆废话…
单调队列优化,第三维枚举体积:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2e4+5;
int n, m;
int f[N], g[N], q[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof f); // i-1层的状态备份,滚动数组
for (int j = 0; j < v; ++j) { // 枚举余数
int hh = 0, tt = -1; // 单调队列定义
for (int k = j; k <= m; k += v) { // 枚举第i个物品所占背包体积,类比数轴
if (hh <= tt && q[hh] < k - s * v) hh ++; // 滑出窗口,队头出队。此时队列元素超过了s个
// 单调队列出队,维护队列单调性
while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt --;
if (hh <= tt) f[k] = max(f[k], g[q[hh]] + (k - q[hh]) / v * w); // 更新最大值
q[++tt] = k; // 插入当前元素
}
}
}
cout << f[m] << endl;
return 0;
}
简单改写,第三维枚举个数代码:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2e4+5;
int n, m;
int f[N], g[N], q[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
int v, w, s;
cin >> v >> w >> s;
memcpy(g, f, sizeof f);
for (int j = 0; j < v; ++j) {
int hh = 0, tt = -1;
for (int k = 0; k <= (m - j) / v; k ++) { // 枚举个数,可直接理解成在数轴上的下标
if (hh <= tt && k - q[hh] > s) hh ++;
while (hh <= tt && g[q[tt] * v + j] - q[tt] * w <= g[k * v + j] - k * w) tt --;
q[++tt] = k; // 先入队
f[k * v + j] = max(f[k * v + j], g[q[hh] * v + j] + (k - q[hh]) * w); // 在此不必再判断,队头即为最大元素
}
}
}
cout << f[m] << endl;
return 0;
}
4. 分组背包
特点:
- 物品有
n
组,每组物品有若干个 - 每组物品最多只能选一件物品
完全背包问题:枚举第 i
件物品选几个
分组背包问题:枚举第 i
组物品选哪个
思路:
- 状态表示:
f[i][j]
从前i
组物品中选择且总体积不大于j
的最大价值 - 状态计算:
- 针对第
i
组物品,将整个状态划分成s[i]+1
类: - 不选第
i
组物品:f[i][j] = f[i-1][j]
- 选第
i
组物品的第一个物品:f[i][j] = f[i-1][j-v[1]]+w[1]
- 选第
i
组物品的第二个物品:f[i][j] = f[i-1][j-v[2]]+w[2]
- 选第
i
组物品的第s[i]
个物品:f[i][j] = f[i-1][j-v[s[i]]+w[s[i]]
最终就是该物品数量的最大限制。十分类似于完全背包问题 - 故状态转移方程为:
f[i][j]=max(f[i][j], f[i-1][j-v[k]]+w[k])), k=0, 1, 2,...s[i]
- 针对第
- 状态初始化:
f[0][0~m] = 0
表示在选择 0 件物品时对于任何体积来讲,其最大价值均为 0
同理,分组背包问题也是可以从二维优化到一维的。其实只需要谨记一点:
- 当我们当前状态需要用上层状态进行转移时,就从大到小枚举体积
- 当我们当前状态需要用本层状态进行转移时,就从小到大枚举体积
这就直接扔上来一维版本了,二维都写了这么多遍了,抓住代码思想,代码实现都大同小异。
分组背包终极优化版本代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 105;
int n, m;
int v[N][N], w[N][N], s[N];
int 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 (j >= v[i][k])
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
return 0;
}
5. 二维费用背包
二维费用背包问题可和 01 背包、完全背包、多重背包、分组背包等背包模型进行简单拼接拓展。
这个每个物品也只被选 1 次,是完全基于 01 背包的二维费用背包问题。
相较于朴素 01 背包状态表示,本题增加了一个重量限制。那么再开一维表示该状态即可。
思路:
- 状态表示:
f[i,j,k]
从前i
个物品中选择且总体积不大于j
,总重量不超过k
的选法的最大价值 - 状态计算:
- 将整个状态划分成两类:
- 不选第
i
个物品:f[i,j,k] = f[i-1,j,k]
- 选第
i
个物品:f[i,j,k] = f[i-1,j-v[i],k-m[i]]+w[i]
- 答案:
f[N,V,M]
同理与 01 背包从 2 维优化成 1 维,在此可由 3 维优化为 2 维,不需要存储 i
,但是体积需要从大到小循环。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005, V = 105, M = 105;
int n, v, m;
int f[V][M];
int main() {
cin >> n >> v >> m;
for (int i = 0; i < n; ++i) {
int vi, mi, wi;
cin >> vi >> mi >> wi;
for (int j = v; j >= vi; --j) {
for (int k = m; k >= mi; --k)
f[j][k] = max(f[j][k], f[j - vi][k - mi] + wi);
}
}
cout << f[v][m] << endl;
return 0;
}
6. 混合背包
重点: 01背包、完全背包、多重背包
这三个背包问题具有相同的状态表示,和集合属性。f[i][j]
都表示只从前 i
件物品中选,且总体积不超过 j
的所有选法的最大价值。在此简单回顾下状态转移方程即可:
- 01 背包
f[i][j] = max(f[i-1][j], f[i-1][j - vi]+wi)
- 完全背包
f[i][j] = max(f[i-1][j], f[i][j-vi]+wi)
- 多重背包
f[i][j] = max(f[i-1][j], f[i-1][j-vi]+wi,f[i-1][j-2*vi]+2*wi,...)
其实 01 背包就是多重背包各个物品数量限制为 1 的背包问题,所以 01 背包很容易就可以转化为多重背包。
本题需要采用多重背包的二进制优化,具体可以参考本博文多重背包部分。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1005;
int n, m;
int f[N];
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
int v, w, s;
cin >> v >> w >> s;
if (s == 0) { // 完全背包
for (int j = v; j <= m; ++j)
f[j] = max(f[j], f[j - v] + w);
} else {
if (s == -1) s = 1; // 01背包就是多重背包数量为1的情况
for (int k = 1; k <= s; k *= 2) { // 多重背包二进制优化
for (int j = m; j >= k * v; --j)
f[j] = max(f[j], f[j - k * v] + k * w);
s -= k;
}
if (s) { // 处理剩下的物品个数
for (int j = m; j >= s * v; --j)
f[j] = max(f[j], f[j - s * v] + s * w);
}
}
}
cout << f[m] << endl;
return 0;
}
7. 有依赖的背包问题
简单版: [分组背包] 金明的预算方案(分组背包+二进制枚举+有依赖背包+思维)
树形 dp
:[树形dp] 没有上司的舞会(模板题+树形dp)
金明的预算方案,这个依赖关系就是主件和附件,且附件最多也就两个,树结构高度为 2。而这个依赖关系就是一颗树,更加复杂。是一个树形 dp
题目,可以参考没有上司的舞会。关于树的题目,都是采用数组模拟邻接表来实现存储的。
有依赖背包问题和单调队列优化多重背包问题是背包九讲中最难的两个问题。
若选当前节点的子树中某一个,那么当前节点就必须先被选了,所以就得递归的考虑该问题,树形 dp
。
思路:
- 状态表示:
f[u][j]
从以u
为根的子树中选,且总体积不超过j
的方案的最大价值。这是和线性前i
个物品不同之处,一个是线性、一个是树形 - 状态计算:
- 以
u
为根节点的子树中选,那么u
肯定是要选的。但是,u
的子节点还是很多的,假设有p
个,那么就有2^p
种情况,是指数级别的。我们在金明中,之所以能够 2 进制枚举,也得益于p
很少,在此肯定是不行的 - 考虑按体积枚举所有的子节点选择情况,这里需要声明,假设
u
节点有 3 个子节点,那么这 3 个子节点递归考虑的话是独立的。我们需要按体积分别划分节点 1、节点 2、节点 3。此时划分的就需要留出u
的体积,故体积不是从m
开始,而是从m-v[u]
开始,枚举体积的话就有0~m-v[u]
种选法,再这些选法中取价值最大值即可。故,这样枚举就将情况从2^p
降到0~m-v[u]
- 那么,一个根节点
u
,包含 3 个子树,这些子树中的所有状态按体积划分为0~m-v[u]
种情况,将这些情况看成状态,那么每个子树都有这么多情况,且这些情况只能被选出来最大的一个。所以,子树就是物品组,这些情况就是物品,将其转化为一个分组背包问题 - 故,递归考虑每个子树的过程中,就是一个分组背包问题。在每个子树内部采用分组背包问题做一遍就行了
- 以
还是蛮抽象的…,代码做了详细的注释。几点需要注意:
- 这里
f
开成 2 维,但是体积仍要从大到小逆序枚举,因为这里还是需要上层的状态,和 01 背包类似,不逆序体积就滚动数组。反正不能当前层更新当前层 - 注意将根节点的价值加上
- 注意将不合法情况,即体积小于根节点的情况清空
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 105;
int n, m;
int v[N], w[N];
int h[N], e[N], ne[N], idx;
int f[N][N]; // 虽然开了二维,不过第一维是根节点数,对于分组背包部分来说还是一维,需要逆序体积
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u) {
for (int i = h[u]; ~i; i = ne[i]) { // 循环物品组,遍历u号点的所有子树,~i表示i!=-1,因为~(-1) = 0
int son = e[i]; // 子节点编号
dfs(e[i]); // 递归所有子树,得到所有子节点的f值
// 分组背包
// 这里对于树中的每个节点来说,就是一个分组背包问题。每个子节点是一组,
// 每个子节点的不同体积和每个体积所对应的最大价值,就是这个物品组中的选出的物品
for (int j = m - v[u]; j >= 0; --j) // 循环体积,根节点一定被先选,留出u的体积。这里是逆序体积,需要上层状态
for (int k = 0; k <= j; ++k) // 循环决策,根据体积分割集合,故为0~j
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]); // f[son][k]代表的是当前子节点k决策的价值
}
for (int i = m; i >= v[u]; --i) f[u][i] = f[u][i - v[u]] + w[u]; // 得加上根节点的价值
for (int i = 0; i < v[u]; ++i) f[u][i] = 0; // 若根节点放不下,则依赖关系不满足,该得到的价值失效
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
int root;
for (int i = 1; i <= n; ++i) { // 读取n个物品
int p; // p为第i个物品的依赖物品编号,可以理解为父节点
cin >> v[i] >> w[i] >> p;
if (p == -1) root = i; // 存根节点
else add(p, i); // p指向i的边,父节点指向i这个子节点
}
dfs(root);
cout << f[root][m] << endl;
return 0;
}