一、01背包
1、朴素做法 O(nm)
f[i][j]
表示前i个物品,体积和不超过j的情况下所能拿的最大价值。
考虑第i个物品,两种情况:取和不取
不取:f[i][j] = f[i - 1][j]
取:f[i][j] = f[i - 1][j - v[i]] + w[i]
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[N][M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &v[i], &w[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (j - v[i] >= 0)
f[i][j] = max(f[i-1][j], f[i - 1][j - v[i]] + w[i]);
else
f[i][j] = f[i-1][j];
}
}
printf("%d\n", f[n][m]);
}
2、滚动数组去掉第一维
观察两个递推式
递推式1:f[i][j] = f[i - 1][j]
递推式2:f[i][j] = f[i - 1][j - v[i]] + w[i]
可以看到递推式f[i]
只用到了f[i-1]
,f[1]~f[i-2]
再也不会用到,显然可以将f的i维从1至n缩减成1至2,即f[i]
可以覆盖f[i-2]
的位置。
更进一步看,f的i维可以缩减成1。
对于递推式1,直接用f[i][j]
覆盖掉f[i-1][j]
的位置,也就是去掉第一维,i仍然是从小到大,转移仍可以顺利进行,因为 f[j]
不会在后面被用到。递推式变成f[j] = f[j]
,这是一个恒等式,可以不写。
对于递推式2,如果j是从小到大,去掉第一维会有问题,因为f[j]
在用f[j-v[i]]
时,f[j - v[i]]
已经在这层循环被更新过了,也就是f[i][j]
用的是f[i][j - v[i]]
而不是f[i - 1][j - v[i]]
,和原来的递推式不等价。
那么如何防止
f[j]
用f[j - v[i]]
时f[j - v[i]]
被更新呢?只需让j从大到小循环即可。
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &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]);
}
}
printf("%d\n", f[m]);
}
二、完全背包
相比01背包,每个物品能取任意个
题目链接
1、朴素做法 O(nm^2)
枚举第i个物品是取0, 1, 2…个
f[i][j] = f[i - 1][j - k * v[i]] + k * w[i]
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[N][M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &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 - k * v[i]] + k * w[i]);
}
}
}
printf("%d\n", f[n][m]);
}
2、去掉内层循环 O(nm)
原本的递推式是
递推式1: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]
的递推式(下划线方便与上面对齐)
递推式2:f[i][j-v] = max(_________f[i-1][j-v], ____f[i-1][j-2v] + w, _f[i-1][j-3v] + 2w, ...)
对比发现
递推式1的第2项f[i-1][j-v] + w
是 递推式2的第1项f[i-1][j-v]
加上w
递推式1的第3项f[i-1][j-2v] + 2w
是 递推式2的第2项f[i-1][j-2v] + w
加上w
以此类推。
将递推式2代入递推式1,(递推式1的第2项之后全替换成 递推式2的第1项之后 + w)
max(f[i-1][j-v] + w, f[i-1][j-2v] + 2w, f[i-1][j-3v] + 3w ... )
= max(f[i-1][j-v], f[i-1][j-2v] + w, f[i-1][j-3v] + 2w, ... ) + w
= f[i][j-v] + w
故递推式变成f[i][j] = max(f[i-1][j], f[i][j-v]+w)
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[N][M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &v[i], &w[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
f[i][j] = max(f[i][j], f[i - 1][j]);
if (j >= v[i])
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
printf("%d\n", f[n][m]);
}
3、滚动数组去掉第一维
和01背包不同,这边f[i][j]
就是从f[i][j - v[i]]
递推而来,要的是这层循环更新后的数据,因此j
要从前往后。所以直接去掉第一维即可。
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], f[M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &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]);
}
}
printf("%d\n", f[m]);
}
三、多重背包
和完全背包不同,多重背包每件物品数量有限制,不能取无限个
题目链接
1、朴素做法 O(nms)
和完全背包差不多,k循环加个条件 k <= 物品个数s[i]即可
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], s[N], f[N][M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &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 <= s[i] && k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
printf("%d\n", f[n][m]);
}
2、滚动数组去掉第一维
同01背包,j需要从大到小
#include <iostream>
using namespace std;
const int N = 1005, M = 1005;
int n, m, v[N], w[N], s[N], f[M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &v[i], &w[i], &s[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
}
}
}
printf("%d\n", f[m]);
}
3、二进制优化 O(nmlogs)
首先多重背包不能像完全背包一样直接去掉内重循环
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-1][j-sv] + sw)
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-sv] + (s-1)w, f[i-1][j-(s+1)v] + sw)
由于多了最后一项f[i-1][j-(s+1)v] + sw
,所以并不能直接用f[i][j-v]
去取代f[i][j]
递推式第2项之后的最大值。
二进制优化是将物品的数目拆分成多组,各组数目是1,2,4,8,16…,每组至多只能选一次。因为1,2,4,8,16…可以凑出任意正整数,例如7可拆成4+2+1,100可拆成64+32+4,都不选就是0。
假如把s拆成1, 2, 4, 8, …, 2k, c,有c < 2k+1 。首先1, 2, 4, 8, …, 2k 可以凑出[0, 2k+1-1]中的任意数,拿这些数+c后可凑出[c, 2k+1-1+c]的任意数,而2k+1-1+c = s,也就是[c, s],又因为c < 2k+1 ,因此[0, 2k+1-1]和[c, s]包括了[0, s]
综上1, 2, 4, 8, …, 2k 可以凑出0 ~ s。
因此把物品数s拆成多组二进制的形式,并用01背包的做法就可以解决多重背包。这样循环k就从O(s)
优化成了O(logs)
。
#include <iostream>
using namespace std;
const int N = 25005, M = 2005;
int n, m, v[N], w[N], f[M];
int main() {
scanf("%d%d", &n, &m);
int cnt = 0, tv, tw, s;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &tv, &tw, &s);
int k = 1;
while (k <= s) {
cnt++;
v[cnt] = tv * k;
w[cnt] = tw * k;
s -= k;
k *= 2;
}
if(s > 0) {
cnt++;
v[cnt] = tv * s;
w[cnt] = tw * s;
}
}
n = cnt;
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]);
}
}
printf("%d\n", f[m]);
}
4、单调队列优化 O(nm)
再看这几个递推式
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-1][j-sv] + sw)
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-sv] + (s-1)w, f[i-1][j-(s+1)v] + sw)
…
简单表示为
f[j]=max(f[j], f[j-v], f[j-2v], f[j-3v], ... , f[j-sv])
f[j-v] = max(__f[j-v], f[j-2v], f[j-3v], ... , f[j-sv], f[j-(s+1)v])
f[j-2v] = max(_________f[j-2v], f[j-3v], ... , f[j-sv], f[j-(s+1)v], f[j-(s+2)v])
f[j-3v] = max(_________________ f[j-3v], ... , f[j-sv], f[j-(s+1)v], f[j-(s+2)v], f[j-(s+3)v])
其实这相当于一个长度为s+1的滑动窗口求最大值问题,可以在循环j的同时用单调队列求窗口内最大值。
(而完全背包则是求前缀最大值,所以用1个数不断更新记录最大值即可,这个数恰好是f[i][j-v]
)
具体实现可用单调队列存f[i][j]
的下标j
(也就是体积)而不是最大值。
用下标可直接算出状态转移式。具体如下:
原本的递推式是f[i][j] = f[i-1][j-kv] + kw
。不妨设窗口最大值(单调队列队头)存的下标是j'
。j' = j - kv
,于是k = (j - j') / v
,递推式变为f[i][j] = f[i - 1][j'] + (j - j') / v * w
。
注意递推式的每项加的w数不同,第1项是0w,第二项是1w,第三项是2w…,j越前加的w越多,因此单调队列进队时需要考虑这点,例如j-2v
和j-5v
就差了3w。具体地,j1
跟j2
比较时(j1 > j2)
,需要比较f[i - 1][j1]
和f[i - 1][j2] + (j1 - j2) / v * w
大小。
同时窗口每向右挪动一格,递推式各项都要+w,但各项+w不会改变窗口最大值是哪一项。
不使用滚动数组
#include <iostream>
using namespace std;
const int N = 1005, M = 20005;
int n, m, v[N], w[N], s[N], f[N][M];
int que[M], head, tail; //队列存的元素是下标(体积),即f[i][j]的j
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) {
scanf("%d%d%d", &v[i], &w[i], &s[i]);
}
for (int i = 1; i <= n; i ++) {
for (int j = 0; j < v[i]; j ++) {
head = 0, tail = -1;
for (int k = j; k <= m; k += v[i]) {
// 队列不为空,且队头下标在窗口外,队头出队
if (head <= tail && que[head] < k - s[i] * v[i]) head++;
// 进队,考虑不同项加的w个数不同
while (head <= tail &&
f[i-1][k] >= f[i - 1][que[tail]] + (k - que[tail]) / v[i] * w[i]) tail --;
que[++ tail] = k;
// 更新f[i][k]
if (head <= tail) {
f[i][k] = max(f[i][k], f[i - 1][que[head]] + (k - que[head]) / v[i] * w[i]);
}
}
}
}
printf("%d\n", f[n][m]);
}
滚动数组保留两行
只需保留f[i]
和f[i - 1]
即可,用f
表示f[i]
,g
表示f[i - 1]
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1005, M = 20005;
int n, m, v[N], w[N], s[N], f[M], g[M];
int que[M], head, tail; //队列存的元素是下标(体积),即f[i][j]的j
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) {
scanf("%d%d%d", &v[i], &w[i], &s[i]);
}
for (int i = 1; i <= n; i ++) {
memcpy(g, f, sizeof(f));
for (int j = 0; j < v[i]; j ++) {
head = 0, tail = -1;
for (int k = j; k <= m; k += v[i]) {
// 队列不为空,且队头下标在窗口外,队头出队
if (head <= tail && que[head] < k - s[i] * v[i]) head++;
// 进队,考虑不同项加的w个数不同
while (head <= tail &&
g[k] >= g[que[tail]] + (k - que[tail]) / v[i] * w[i]) tail --;
que[++ tail] = k;
// 更新f[i][k]
if (head <= tail) {
f[k] = max(f[k], g[que[head]] + (k - que[head]) / v[i] * w[i]);
}
}
}
}
printf("%d\n", f[m]);
}
四、混合背包问题
物品有3类,分别是只能选1次、选无限次、选si次。相当于01背包、完全背包、多重背包混合。
题目链接
还是分两类,取第i件物品和不取第i件物品。
1、不取第i件物品:f[i][j] = f[i - 1][j]
2、取第i件物品:看第i件物品的类别,如果只选1次用01背包的递推式,选无限次按完全背包的递推式,选si次按多重背包的递推式。
2.1、选1次:f[i][j] = f[i - 1][j - v[i]] + w[i]
2.2、选无限次:f[i][j] = f[i][j - v[i]] + w[i]
2.3、选si次:f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i], f[i - 1][j - 2v[i]] + 2w[i], ...)
,数据量较大可用多重背包的二进制优化。
#include <iostream>
using namespace std;
const int MAX_N = 1005 * 10, MAX_V = 1005;
int N, V, v[MAX_N], w[MAX_N], s[MAX_N], f[MAX_V];
int main() {
scanf("%d%d", &N, &V);
int cnt = 0, tv, tw, ts;
for (int i = 1; i <= N; i++) {
scanf("%d%d%d", &tv, &tw, &ts);
if (ts == -1 || ts == 0) {
cnt++;
v[cnt] = tv;
w[cnt] = tw;
s[cnt] = ts;
}
else {
int k = 1;
while (k <= ts) {
cnt++;
v[cnt] = tv * k;
w[cnt] = tw * k;
s[cnt] = k;
ts -= k;
k *= 2;
}
if(ts > 0) {
cnt++;
v[cnt] = tv * ts;
w[cnt] = tw * ts;
s[cnt] = k;
}
}
}
N = cnt;
for (int i = 1; i <= N; i++) {
if (s[i] != 0) { // 01或多重背包
for (int j = V; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
else { // 完全背包
for (int j = v[i]; j <= V; j++) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
}
printf("%d\n", f[V]);
}
更优雅的写法(其实就是不存输入)
#include <iostream>
using namespace std;
const int MAX_N = 1005 * 10, MAX_V = 1005;
int N, V, f[MAX_V];
int main() {
scanf("%d%d", &N, &V);
int v, w, s;
for (int i = 1; i <= N; i++) {
scanf("%d%d%d", &v, &w, &s);
if (s == -1) s = 1;
if (s) { // 01或多重背包
for (int k = 1; k <= s; k *= 2) {
for (int j = V; j >= k * v; j--) {
f[j] = max(f[j], f[j - k * v] + k * w);
}
s -= k;
}
if (s) {
for (int j = V; j >= s * v; j--) {
f[j] = max(f[j], f[j - s * v] + s * w);
}
}
}
else {// 完全背包
for (int j = v; j <= V; j++) {
f[j] = max(f[j], f[j - v] + w);
}
}
}
printf("%d\n", f[V]);
}
五、二维费用背包问题 O(nvm)
除了体积限制以外,还有重量限制。
题目链接
f[i][j][k]
表示前i个物品,体积不大于j,重量不大于k的价值和最大。
可以分成两种,选第i组物品和不选第i组物品
1、不选第i个物品:f[i][j][k] = f[i-1][j][k]
2、选第i个物品:f[i][j][k] = f[i-1][j - v[i]][k - m[i]] + w[i]
写法基本和01背包相同。也可用滚动数组去掉第一维。
#include <iostream>
using namespace std;
const int NAX_N = 1005, MAX_V = 105, MAX_M = 105;
int N, V, M, f[MAX_V][MAX_M];
int main() {
scanf("%d%d%d", &N, &V, &M);
int v, m, w;
for (int i = 1; i <= N; i++) {
scanf("%d%d%d", &v, &m, &w);
for (int j = V; j >= v; j--) {
for (int k = M; k >= m; k--)
f[j][k] = max(f[j][k], f[j - v][k - m] + w);
}
}
printf("%d\n", f[V][M]);
}
六、分组背包 O(nm)
每组有物品若干个,同一组内的物品最多只能选一个。
题目链接
1、朴素做法
f[i][j]
表示前i组物品,体积不大于j的价值和最大。
可以分成两种,选第i组物品和不选第i组物品
1、不选第i组物品:f[i][j] = f[i-1][j]
2、选第i组物品:枚举选的是第i组的哪个物品
f[i][j] = f[i-1][j - v[i][k]] + w[i][k]
v[i][k]、w[i][k]表示i组第k个物品的体积、价值
写法基本和多重背包相同。
#include <iostream>
using namespace std;
const int N = 105, S = 105, M = 105;
int n, m, v[S][N], w[S][N], s[N], f[N][M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &s[i]);
for (int j = 1; j <= s[i]; j++) {
scanf("%d%d", &v[i][j], &w[i][j]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
f[i][j] = f[i - 1][j];
for (int k = 1; k <= s[i]; k++) {
if (j - v[i][k] >= 0)
f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
}
}
}
printf("%d\n", f[n][m]);
}
2、滚动数组去掉第一维
同01背包,j从大到小
#include <iostream>
using namespace std;
const int N = 105, S = 105, M = 105;
int n, m, v[S][N], w[S][N], s[N], f[M];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &s[i]);
for (int j = 1; j <= s[i]; j++) {
scanf("%d%d", &v[i][j], &w[i][j]);
}
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 1; k <= s[i]; k++) {
if (j - v[i][k] >= 0)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
printf("%d\n", f[m]);
}
七、有依赖背包问题 O(nm^2)
购买某个物品必须要把它的前置物品给买了。
题目链接
由于物品之间有相互依赖关系,这个依赖关系呈树形,因此考虑用树形DP解决。
f[i, j]
表示:以第i
个物品为根节点的子树,选物品i,总体积不超过j
的方案的最大价值。
这边条件加上必须选取i是因为只有购买了i才能考虑购买i的子树。
考虑最后状态如何转移,不像01背包那样只要考虑取或不取上一个,这边k个孩子可以选取其中任意个,那么可能的方案就是2k种,将这些方案都考虑一遍显然会超时。
因此不再考虑要选取几个孩子,而是考虑选k体积方案的孩子,其中0 <= k <= m
。
因为体积j
的最优方案必然是从某一个k
转移过来,而这边k
有m+1种,这就类似分组背包问题,将m+1个k看成1组,从这组中挑选1个转移。
不妨设i的孩子是s1、s2、…、sn,因此转移方程式就变成
f[i][j] = f[i][j-k] + max(f[s1][k], f[s2][k], ..., f[sn][k])
这样乍一看好像是只挑了一个孩子,不符合我们取任意几个孩子的决策,其实不然。这只是针对当前体积从这个孩子里取最大的,体积是其他的时候还会包括选其他孩子的方案。
举个例子,不妨假设最优解是挑s1
、s2
,他们的体积分别是k1
、k2
。
那么选s1
体积k1
的方案,在递归s1
时已经包括在f[i][j-k2]
中了;递归s2
时f[i][j-k2]
再加上选s2
体积k2
的方案就得到f[i][j]
。
PS1:这边的j
要从大到小枚举,不然同一组的物品可能选多次
PS2:因为f[i, j]为必须购买i,因此j循环范围是从V-v[i]到0,要先预留物品i的空间
PS3:在遍历完所有孩子后,v[i] <= j <=V
的部分必须加上物品i,即f[i][j] = f[i][j - v[i]] + w[i]
,注意不是f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i])
,因为不买i是不符合f的定义的
PS4:在遍历完所有孩子后,要对j<= v[i]
的部分清0,因为这部分体积的方案只能买子树而不能买物品i,不符合题意
#include <iostream>
#include <cstring>
using namespace std;
int N, V, v[105], w[105], f[105][105];
int head[105], val[105], nxt[105], len;
void add_node(int a, int b) {
val[len] = b;
nxt[len] = head[a];
head[a] = len ++;
}
void dfs(int u) {
for (int i = head[u]; i != -1; i = nxt[i]) {
int son = val[i];
dfs(son);
for (int j = V - v[u]; j >= 0; j--) {
for (int k = 0; k <= j; k++) {
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
for (int j = V; j >= v[u]; j--) f[u][j] = f[u][j - v[u]] + w[u];
for (int j = v[u] - 1; j >= 0; j--) f[u][j] = 0;
}
int main() {
scanf("%d%d", &N, &V);
int p, root;
memset(head, -1, sizeof(head));
for (int i = 1; i <= N; i ++) {
scanf("%d%d%d", &v[i], &w[i], &p);
if (p == -1) {
root = i;
continue;
}
add_node(p, i);
}
dfs(root);
printf("%d\n", f[root][V]);
return 0;
}
八、泛化物品
九、背包问题问法的变化
1、不同条件的背包(体积限制改成不少于/恰好是,求价值由最大改为最小等)
1、初始化不同。
初始化只需看i=0
(一个都不选)的情况,i>0
可由i=0
递推而来。
“不超过” 的初始化要将f[0][j]
全部设为0,是因为体积不超过j,包括了体积为0的选法。
“恰好是” 的初始化只有f[0][0]=0
,其余都是无穷,因为一个都不选体积只能是0,不能是其他值。
“至少是” 的初始化只有f[0][0]=0
,其余都是无穷。因为一个都不选体积只能是0,所以只有体积至少是0才是合法的。
2、j的循环范围不同
“不超过”和“恰好是”的情况下,j - v[i] < 0
是无意义的,体积不超过-1和恰好是-1都是不可能的选法,因为体积最小就是0。
”至少是“的情况下,当j - v[i] < 0
的时候仍然可以进行状态转移,因为体积至少是-1、-2、-3也是有意义的,它包括了体积>=0的选法。可将负数的部分答案记在f[i][0]
中。
体积条件 | 写法 |
---|---|
不超过 | f全初始化为0;循环时保证j-v[i]>=0 |
恰好是 | f[0]=0 ,其余无穷;循环时保证j-v[i]>=0 |
至少是 | f[0]=0 ,其余无穷;循环时不必保证j-v[i]>=0 |
ps1.初始化取正无穷还是负无穷取决于问题求价值最大值还是最小值,求最大取负无穷,求最小取正无穷。
ps2. 如果不是求价值问题,初始化0和无穷可根据题意更改,例如求方案数往往f[0][0] = 1
,其余是0。
2、背包问题求具体方案(字典序最小)
题目链接
以01背包为例,状态转移只有两种可能
不取第i件物品:f[i][j] = f[i - 1][j]
取第i件物品:f[i][j] = f[i - 1][j - v[i]] + w[i]
如果取和不取的得到的答案一样,那就选取的方案,这样输出的字典序会更小。(字典序只要相同位置小就更小,跟长度无关,’\0’当成最小的,例如123<21,12<123)因为选了i在放这位,肯定比不选i,选i之后的放在这位更好。例如i是3,选了i是1235,不选i是125,1235 < 135。
具体实现时,比较f[i][j]
和f[i - 1][j - v[i]] + w[i]
的值,相同则输出i。但这样就是从后往前推,输出的方案会优先包括n更大的,也就是将字典序最大的解倒序输出。
要想输出字典序最小的解,只需要把输入顺序进行反向,输出N - i + 1即可,也就是把第1个物品当成第n个,第2个物品当成第n - 1个…
#include <iostream>
using namespace std;
const int MAX_N = 1005, MAX_V = 1005;
int N, V, f[MAX_N][MAX_V], v[MAX_N], w[MAX_N];
int main() {
scanf("%d%d", &N, &V);
for (int i = N; i >= 1; i--) {
scanf("%d%d", &v[i], &w[i]);
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= V; j++) {
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
int j = V;
for (int i = N; i >= 1; i--) {
if (j >= v[i] && f[i][j] == f[i - 1][j - v[i]] + w[i]) {
printf("%d ", N - i + 1);
j -= v[i];
}
}
}
广为流传的貌似是另一种写法,不改变输入顺序和输出,改变递推式推导的方向,从第一个往后推,也可以达到相同的效果。
#include <iostream>
using namespace std;
const int MAX_N = 1005, MAX_V = 1005;
int N, V, f[MAX_N][MAX_V], v[MAX_N], w[MAX_N];
int main() {
scanf("%d%d", &N, &V);
for (int i = 1; i <= N; i++) {
scanf("%d%d", &v[i], &w[i]);
}
for (int i = N; i >= 1; i--) {
for (int j = 1; j <= V; j++) {
f[i][j] = f[i + 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
}
}
int j = V;
for (int i = 1; i <= N; i++) {
if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i]) {
printf("%d ", i);
j -= v[i];
}
}
return 0;
}
3、背包问题求体积为V的方案数
把递推式的max改成相加即可。
01背包可参考:数字组合【求体积恰好为V的方案数】
完全背包可参考:买书【体积恰好是V求方案数】
4、背包问题求最优解(最大价值)的方案数
求的是取到最大价值的方案数
题目链接
开一个数组g[i, j]
表示f[i, j]
取最大值的方案数。
考虑是第i个物品取的价值大还是不取的价值大。
不取价值大:f[i, j] = f[i - 1, j]
,g[i, j] = g[i-1, j]
取价值大:f[i, j] = f[i-1, j-v[i]] + w[i]
,g[i, j] = g[i-1, j-v[i]]
取和不取一样大:g[i, j] = g[i-1, j] + g[i-1, j-v[i]]
注意这边g[i, j]
只表示f[i, j]
的方案数,条件是“前i
个物品,体积恰好是j
的最大价值方案数”,可能f[i', j']
也能达到最优解,这种方案数记录在g[i', j']
中,因此要把所有能达到最优解的f
对应的g
累加起来
#include <iostream>
using namespace std;
int N, V;
int f[1005], g[1005];
const int mod = 1e9 + 7;
int main() {
scanf("%d%d", &N, &V);
int v, w;
g[0] = 1;
int max_f = 0;
for (int i = 1; i <= N; i ++) {
scanf("%d%d", &v, &w);
for (int j = V; j >= v; j --) {
if (f[j] < f[j - v] + w) {
f[j] = f[j - v] + w;
g[j] = g[j - v];
}
else if(f[j] == f[j - v] + w) {
g[j] = (g[j] + g[j - v]) % mod;
}
}
}
int ans = 0;
for (int i = 0; i <= V; i ++) {
if (f[i] == f[V]) ans = (ans + g[i]) % mod;
}
printf("%d\n", ans);
return 0;
}