先循环物品,在循环体积,再循环决策。
01背包
给n个物品,每个物品有价值
V
i
V_i
Vi, 体积
W
i
W_i
Wi。
给一个背包,总容量
m
m
m。
问,背包可以装的最大价值(物品不可拆)
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N];
int main() {
int n, m;
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-1][j-v[i]]+w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
空间优化
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int f[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=m; j>=v[i]; j--) {
f[j] = max(f[j], f[j-v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
空间再优化
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1010;
int v, w, f[N];
int n, m;
int main() {
cin >> n >> m;
for(int i=1; i<=n; i++) {
cin >> v >> w;
for(int j=m; j>=v; j--) {
f[j] = max(f[j], f[j-v] + w);
}
}
cout << f[m] << endl;
return 0;
}
01背包很重要。
01背包枚举每个物品,每个物品有两种选择:被装入背包,没被装入背包。
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示前i个物品在j体积下的最大价值。
这张图是 f 数组;它的每一个元素都是由上一行元素转移过来的
转移方程:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
]
,
f
[
i
−
1
]
[
j
−
v
[
i
]
]
+
w
[
i
]
)
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i]) 可以根据转移方程和图推一遍,加深印象。
我们再看下一个版本
f
[
N
]
f[N]
f[N] 。从上面的图中可以看到每一行都是上一行元素转移过来的。
再观察方程发现,第二行的第j个元素只由第一行的第
j
j
j个元素或第
j
j
j个元素和第
j
−
v
[
2
]
j-v[2]
j−v[2]个元素决定(与
j
j
j之前的元素有关)。
所以要从第一行的状态转移到第二行的状态。j就得从最大的
m
m
m开始枚举。假如我们
(下一个背包讲j从
v
[
2
]
v[2]
v[2]到
m
m
m的产生的影响)。
完全背包
给
n
n
n种物品,每个物品有价值
V
i
V_i
Vi, 体积
W
i
W_i
Wi(每种物品可以选多次)。
给一个背包,总容量
m
m
m。
问,背包可以装的最大价值(物品不可拆)
/* n^3 */
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1010;
int n, m;
int f[N], v, w;
int main() {
cin >> n >> m;
for(int i=1; i<n; i++) {
cin >> v >> w;
for(int j=m; j>=v; j--) {
for(int k=1; v*k <= j; k++) {
f[j] = max(f[j], f[j-k*v] + k*w);
}
}
}
cout << f[m] << endl;
return 0;
}
/*
复杂度优化 n^2
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1010;
int f[N], v, w;
int n, m;
int main() {
cin >> n >>m;
for(int i=1; i<=n; i++) {
cin >> v >> w;
for(int j=v; j<=m ;j++) {
f[j] = max(f[j], f[j-v]+w);
}
}
cout << f[m] << endl;
return 0;
}
*/
一维数组可以参照01背包理解
第一个版本可以转换成01背包理解。每种物品无数个,但背包的体积有限,所以每种物品就有个数限制,我们把**
k
k
k个物品看成01背包中的一个**,也就是每种物品选
k
k
k个(保证了在前i种物品j体积价值最大)。
第二个版本用的是两重循环。
j
j
j 从
v
i
v_i
vi 到
m
m
m,看图。
当
i
=
1
i = 1
i=1时,
[
j
]
=
m
a
x
(
f
[
j
]
,
f
[
j
−
v
]
+
w
)
[j] = max(f[j], f[j-v]+w)
[j]=max(f[j],f[j−v]+w);
j
=
1
,
f
[
1
]
=
m
a
x
(
f
[
1
]
,
f
[
1
−
1
]
+
2
)
j = 1,f[1] = max(f[1], f[1-1]+2)
j=1,f[1]=max(f[1],f[1−1]+2);
j
=
2
,
f
[
2
]
=
m
a
x
(
f
[
2
]
,
f
[
2
−
1
]
+
2
)
j = 2, f[2] = max(f[2], f[2-1]+2)
j=2,f[2]=max(f[2],f[2−1]+2);
j
=
3
,
f
[
3
]
=
m
a
x
(
f
[
3
]
,
f
[
3
−
1
]
+
2
)
j = 3, f[3] = max(f[3], f[3-1]+2)
j=3,f[3]=max(f[3],f[3−1]+2);
j
=
4
,
f
[
4
]
=
m
a
x
(
f
[
4
]
,
f
[
4
−
1
]
+
2
)
j = 4, f[4] = max(f[4], f[4-1]+2)
j=4,f[4]=max(f[4],f[4−1]+2);
j
=
5
,
f
[
5
]
=
m
a
x
(
f
[
5
]
,
f
[
5
−
1
]
+
2
)
j = 5, f[5] = max(f[5], f[5-1]+2)
j=5,f[5]=max(f[5],f[5−1]+2);
第1种物品被选了5次,推广第i种物品也可以被选多次。
和01背包比较记忆 j 顺序和逆序遍历的影响。
多重背包(两种优化,只会一种)
给
n
n
n种物品,每个物品有价值
V
i
V_i
Vi, 体积
W
i
W_i
Wi,个数
K
i
K_i
Ki(每种物品可以选
K
i
K_i
Ki次)。
给一个背包,总容量
m
m
m。
问,背包可以装的最大价值(物品不可拆)
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1010;
int f[N], v, w, s;
int n, m;
int main() {
cin >> n >> m;
for(int i=1; i<=n; i++) {
cin >> v >> w >> s;
for(int j=m; j>=v; j--) {
for(int k=1; k<=s&&k*v<=j; k++) {
f[j] = max(f[j], f[j-k*v]+k*w);
}
}
}
cout << f[m] << endl;
return 0;
}
//复杂度优化(二进制优化)
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 2010;
int f[N], v, w, s;
int n, m;
struct Goods {
int v, w;
} good;
vector<Goods> goods;
int main() {
cin >> n >> m;
for(int i=0; i<n; i++) {//预处理
cin >> v >> w >> s;tei
for(int k=1; k<=s; k*=2) {
s -= k;
goods.push_back( {k*v, k*w} );
}
if(s > 0) {
goods.push_back({s*v, s*w});
}
}
for(int i=0; i<goods.size(); i++) {//化作01背包枚举。
for(int j=m; j>=goods[i].v; j--) {
f[j] = max(f[j], f[j-goods[i].v] + goods[i].w);
}
}
cout << f[m] << endl;
return 0;
}
//单调队列优化(还没学会)
*/
第一个版本也可以转换成01背包,从每种物品中的一个物品代表01背包中的一个物品。
第二个版本也是转换成01背包理解,举个例子:
第1件物品有10个;我们拆成1个,2个,4个,3个。这4个数可以组成1~10之内的全部数字。这样一来,我们就可以用这几组物品表示第一种物品,剩下的就是选与不选(==01背包)
混合背包
前面3个背包问题混在一起。
(转化成01背包)
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 1010;
int f[N], v, w;
int n, m, s;
struct Good{
int kind;
int v, w;
};
vector<Good> goods;
int main() {
cin >> n >> m;
for(int i=0; i<n; i++) {
cin >> v >> w >> s;
if(s < 0) {
goods.push_back({-1, v, w});
}
else if(s == 0){
goods.push_back({0, v, w});
}
else {
for(int k=1; k<=s; k*=2) {
s -= k;
goods.push_back({-1, v*k, w*k});
}
if(s > 0) {
goods.push_back({-1, v*s, w*s});
}
}
}
for(int i=0; i<goods.size(); i++) {
if(goods[i].kind == -1) {
for(int j=m; j>=goods[i].v; j--) {
f[j] = max(f[j], f[j-goods[i].v]+goods[i].w);
}
}
else {
for(int j=goods[i].v; j<=m; j++) {
f[j] = max(f[j], f[j-goods[i].v]+goods[i].w);
}
}
}
cout << f[m] << endl;
return 0;
}
这个就是上面3个背包的合集,把每个背包都转换成01背包,再做选择
二维费用背包
给
n
n
n种物品,每个物品有价值
V
i
V_i
Vi, 体积
W
i
W_i
Wi,重量
G
i
G_i
Gi。
给一个背包,总容量
m
m
m,限重
G
G
G。
问,背包可以装的最大价值(物品不可拆)
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 110;
int f[N][N], v, g, w;
int n, m, G;
int main() {
cin >> n >> m >> G;
for(int i=0; i<n; i++) {
cin >> v >> g >> w;
for(int j=G; j>=g; j--) {
for(int k=m; k>=v; k--) {
f[j][k] = max(f[j][k], f[j-g][k-v]+w);
}
}
}
cout << f[G][m] << endl;
return 0;
}
类比01背包(主要是不好画图,不好解释)
分组背包
给
n
n
n组物品,每组 物品有
K
i
K_i
Ki个 价值
V
i
V_i
Vi, 体积
W
i
W_i
Wi。
给一个背包,总容量
m
m
m,从每组物品中最多选一个。
问,背包可以装的最大价值(物品不可拆)
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1010;
int f[N], v[N], w[N];
int n, m;
int main() {
cin >> n >> m;
for(int i=0; i<n; i++) {
int c;
cin >> c;
for(int j=0; j<c; j++) cin >> v[j] >> w[j];
for(int j=m; j>=0; j--) {
for(int k=0; k<c; k++) {
if(j >= v[k]) {
f[j] = max(f[j], f[j-v[k]]+w[k]);
}
}
}
}
cout << f[m] << endl;
return 0;
}
f
[
j
]
f [j]
f[j] 表示在体积
j
j
j下最大价值。
体
积
倒
着
枚
举
确
保
每
组
物
品
选
一
次
(
看
01
背
包
)
体积倒着枚举确保每组物品选一次(看01背包)
体积倒着枚举确保每组物品选一次(看01背包)
第一层循环枚举物品第i个物品组,
第二层循环枚举背包体积,
第三层循环枚举每组物品(决策)。
决策:每组物品中的每个物品都都会参加决策,但会挑选一个最优结果(该状态下)
每个
f
[
j
]
f[j]
f[j] 都从
f
[
j
−
v
[
k
]
]
f[j-v[k]]
f[j−v[k]] 转移过来(在k个决策中选一个最大价值放入
f
[
j
]
f[j]
f[j]),所以
f
[
j
]
f[j]
f[j] 在每组中只会选一个物品(满足最大价值的)。
注意和完全背包 对比,区别(
j
j
j 的遍历顺序决定选的次数)。
有依赖性的背包问题
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N = 110;
struct E{
int t, n;
} Edge[N];
int n, V, r, ro;
int head[N], idx = 0, f[N][N], v[N], w[N];
void add(int x, int i) {
Edge[idx].t = i, Edge[idx].n = head[x], head[x] = idx++;
}
void dfs(int u) {
for(int i=head[u]; i!=-1; i=Edge[i].n) {
int son = Edge[i].t;
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=0; j<v[u]; j++) f[u][j] = 0;
}
int main() {
memset(head, -1, sizeof(head));
cin >> n >> V;
for(int i=1; i<=n; i++) {
cin >> v[i] >> w[i] >> r;
if(r == -1) ro = i;
else add(r, i);
}
dfs(ro);
cout << f[ro][V];
return 0;
}
其中有用到数组模拟邻接表(有兴趣可以去看看我前面的博客)
跳过邻接表构树部分。
我们从根节点递归到树最低层,自下向上去求每个节点的最大价值
(解释一下,每个结点的最大价值包括对它的儿子进行了决策之后的价值和节点本身的价值。)
注:
for(int j=V-v[u]; j>=0; j--)
这个给u节点留了一个空间。
for(int j=V; j>=v[u]; j--) f[u][j] = f[u][j-v[u]]+w[u];
for(int j=0; j<v[u]; j++) f[u][j] = 0;
这两个循环呢,给每个 f [ i ] [ j ] f[i][j] f[i][j]补齐 u u u节点的价值 和 调整一定不存在的 f [ i ] [ j ] f[i][j] f[i][j]。
背包问题求方案数
#include<iostream>
#include<cstdio>
#define N 1010
const int INF = 1000000;
const int mod = 1000000007;
using namespace std;
int f[N], g[N];
int main() {
int n, m, v, w, maxn = -INF, sum = 0;
cin >> n >> m;
g[0] = 1;
for(int i=0; i<=m; i++) f[i] = -INF;
for(int i=0; i<n; i++) {
cin >> v >> w;
for(int j=m; j>=v; j--) {
int t = max(f[j], f[j-v]+w), s = 0;
if(t == f[j]) s += g[j];
if(t == f[j-v]+w) s += g[j-v];
s %= mod;
f[j] = t;
g[j] = s;
}
}
for(int i=0; i<=m; i++) maxn = max(f[i], maxn);
for(int i=0; i<=m; i++) {
if(f[i] == maxn) sum += g[i];
if(sum >= mod) sum -= mod;
}
cout << sum;
return 0;
}
01背包基础上问方案数。
先解释一下把为什么 f 数组赋值 -INF。
这样的作用呢,是为了使f[[j]意义变成恰好在j体积下的最大价值。
(就是这样,我不太懂)
我们再开一个
g
[
N
]
g[N]
g[N]数组用来存
j
j
j体积下最大价值的方案数。
if(t == f[j]) s += g[j];
if(t == f[j-v]+w) s += g[j-v];
s %= mod;
g[j] = s
方案数
g
[
j
]
g[j]
g[j] 的要么是等于在第
i
−
1
i-1
i−1件物品时
g
[
j
]
g[j]
g[j],要么是等于在第
i
−
1
i-1
i−1件物品时
g
[
j
−
v
]
g[j-v]
g[j−v]。
(再解释一下,代码判断的是最大值。假如 f[j] = f[j-v]+w 那么 g[j] = g[j] + g[j-v]; j ,j-v这两种方案都满足第i物品下j体积最大值)
背包问题求具体方案
#include<iostream>
#include<cstdio>
const int N = 1010;
using namespace std;
int f[N][N], v[N], w[N];
int main() {
int n, m;
cin >> n >> m;
for(int i=1; i<=n; i++) cin >> v[i] >> w[i];
for(int i=n; i>=1; 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][j], f[i+1][j-v[i]]+w[i]);
}
}
int vol = m;
for(int i=1; i<=n; i++) {
if(vol-v[i]>=0 && f[i][vol] == f[i+1][vol-v[i]]+w[i]) {
cout << i << ' ';
vol -= v[i];
}
}
return 0;
}
按代码动规,最大价值是
f
[
1
]
[
m
]
f[1][m]
f[1][m]。
我们从前往后判断
f
[
1
]
[
m
]
f[1][m]
f[1][m]是从
f
[
2
]
[
j
]
f[2][j]
f[2][j] 还是从
f
[
2
]
[
j
−
v
[
i
]
]
f[2][j-v[i]]
f[2][j−v[i]] 递推过来的。
······(我们在这可以确保方案满足最大价值)
我们再来解释为啥字典序最小。
我们是从1~n遍历,所以如果前面满足,那么一定会先选前面的
按这样贪心,字典序就最小了。
题目
自我理解,如有错误,请指正。