本篇文章设计的题目均在AcWing的2-12题。
一、01背包
要求:每种物品只能选择0个或1个,即对于每种物品只有选或者不选两种情况。
题目描述:(题目链接)
\quad
有
N
N
N 件物品和一个容量是
V
V
V 的背包。每件物品只能使用一次。第
i
i
i 件物品的体积是
v
i
v_i
vi,价值是
w
i
w_i
wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
输入格式:
\quad
第一行两个整数,
N
,
V
N,V
N,V,用空格隔开,分别表示物品数量和背包容积。接下来有
N
N
N 行,每行两个整数
v
i
,
w
i
v_i,w_i
vi,wi,用空格隔开,分别表示第
i
i
i 件物品的体积和价值。
输出格式:
\quad
输出一个整数,表示最大价值。
数据范围:
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<v_i,w_i≤1000
0<vi,wi≤1000
输入样例:
4 5
1 2
2 4
3 4
4 5
输出样例:
8
思路1:二维数组记录
\quad
f[i][j]
表示只看前i
物品,总体积是j
的情况下总价值最大是多少。最大值答案就在f[n][0-V]
中枚举最大值即可,res=max(f[n][0-V])
。
\quad
假设我们将前i-1
个物品已计算完毕,考虑第i
个物品,体积为j
时,第i
个物品只有两种选择,即选或者不选。
- 若不选,则相当于只考虑前
i-1
个物品且体积为j
的情况,这种情况下f[i][j]=f[i-1][j]
; - 若选,则相当于背包容量只剩下
j-v[i]
,这种情况下f[i][j]=f[i-1][j-v[i]]+w[i]
\quad
最终结果就相当于在这两种情况下取个最大值即可。还有个问题就是其初始化,很简单,就是没有物品给你选择,背包容量为0时f[0][0]=0
。
\quad
时间复杂度和空间复杂度都为
O
(
n
V
)
O(nV)
O(nV)。
程序(CPP):
#include <iostream>
#include <algorithm>
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];
f[0][0] = 0;
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][j], f[i-1][j-v[i]]+w[i]);
}
int res = 0;
for(int i = 0; i <= m; i++)
res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
思路2:滚动数组优化为一维
程序(CPP):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[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 = m; j >= v[i]; j--)
f[j] = max(f[j], f[j-v[i]]+w[i]);
cout << f[m] << endl;
return 0;
}
二、完全背包
要求:每种物品可以选0个或者无限个.
题目描述:(题目链接)
\quad
有
N
N
N 件物品和一个容量是
V
V
V 的背包,每种物品都有无限件可用。第
i
i
i 件物品的体积是
v
i
v_i
vi,价值是
w
i
w_i
wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
输入格式:
\quad
第一行两个整数,
N
,
V
N,V
N,V,用空格隔开,分别表示物品数量和背包容积。接下来有
N
N
N 行,每行两个整数
v
i
,
w
i
v_i,w_i
vi,wi,用空格隔开,分别表示第
i
i
i 件物品的体积和价值。
输出格式:
\quad
输出一个整数,表示最大价值。
数据范围:
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<v_i,w_i≤1000
0<vi,wi≤1000
输入样例:
4 5
1 2
2 4
3 4
4 5
输出样例:
10
思路1:
\quad
f[i][j]
表示只看前i
物品,总体积是j
的情况下总价值最大是多少。最大值答案就在f[n][0-V]
中枚举最大值即可,res=max(f[n][0-V])
。
\quad
假设我们将前i-1
个物品已计算完毕,考虑第i
个物品,体积为j
时,第i
个物品只有多种选择,即选0个,1个,k个。我们可以再加上一重循环,枚举每个物品能选的个数k
,注意k
的范围为(int k=0; k*v[i]<=j; k++)
程序(CPP):
#include <iostream>
using namespace std;
const int N = 1010;
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 = 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]);
int res = 0;
for(int i = 0; i <= m; i++) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
\quad
思路2:利用滚动数组优化到一维,同01背包优化思想
#include <iostream>
using namespace std;
const int N = 1010;
int f[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 = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--)
for(int k = 0; k * v[i] <= j; k++)
f[j] = max(f[j], f[j - k*v[i]] + k*w[i]);
cout << f[m] << endl;
return 0;
}
思路3:
\quad
f[i]
表示总体积是i
的情况下最大价值是多少,最终答案就是max(f[i])
,其实也就是f[最大体积]
,因为体积越大肯定能装下物品的最大价值越高,至少不会降低。这里面体积j
从小到大枚举表示f[i]
可能从第i
个物品转移过来;从大到小枚举的话表示只能从i-1
个物品转移过来。因此完全背包相对于01背包只需要体积从小到大枚举。
程序(CPP):
#include <iostream>
using namespace std;
const int N = 1010;
int f[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 = 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;
}
Note:如果题目问的是恰好用了m
的体积,问最大价值?这时候只需要初始化的时候除了f[0]=0
外其他赋值为-inf
即可。
三、多重背包
要求:每种物品能选择的个数给个限制。
思路1:直接枚举每种物品能选择的个数k
即可。
#include <iostream>
using namespace std;
const int N = 110;
int f[N][N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, num; cin >> v >> w >> num;
for(int j = 0; j <= m; j++)
for(int k = 0; k <= num && k*v <= j; k++)
f[i][j] = max(f[i][j], f[i-1][j - k*v] + k*w);
}
int res = 0;
for(int i = 0; i <= m; i++) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
思路2:滚动数组优化空间
#include <iostream>
using namespace std;
const int N = 110;
int f[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, num; cin >> v >> w >> num;
for(int j = m; j >= v; j--)
for(int k = 0; k <= num && k*v <= j; k++)
f[j] = max(f[j], f[j - k*v] + k*w);
}
cout << f[m] << endl;
return 0;
}
思路3:二进制优化
\quad
假设有a个物品b,那么我们将物品b拆分为a份,每一份看作一个单独的物品,这样就将多重背包转化为01背包啦!但我们需要按照一份一份拆分吗?其实是不必的,比如有7个物品b,那么我们只需要拆分为1个b,2个b,4个b,这样我们能表示出任意一个物品b,假设我们选择6个b,那么就是4+2。即给定n个物品b,我们根据二进制拆分法能拆出来log(n)向上取整个数,这些数能表示出[0-n]中任意一个数。借助于这个思想,我们就可以进行优化,假设有n个物品,背包容量为V,每种物品个数不超过num,则时间复杂度由暴力的
O
(
n
V
∗
n
u
m
)
O(nV*num)
O(nV∗num)变为
O
(
n
V
∗
l
o
g
(
n
u
m
)
)
O(nV*log(num))
O(nV∗log(num))。
\quad
这里还有个问题,如何找出
l
o
g
(
n
)
向
上
取
整
log(n)向上取整
log(n)向上取整个数使得这些数的组合能表示[0,n]中任意一个数。假设
n
=
10
n=10
n=10,我们取前三个数为1,2,4,最后一个数不能是8,因为是8的话就能表示出[0-15]间任意一个数,而每种物品个数不能超过10。那最后一个数怎么选呢?其实这个数就是10-1-2-4=3,因为1,2,4能表示出[0-7]间任意一个数,再拿出一个数3,与[0-7]间数相加,就可以得到[8-10]间任意一个数且不会超过10,因此1,2,4,3这4个数,就可以拼出任意[0-10]区间的数。
程序(CPP):
#include <iostream>
#include <vector>
using namespace std;
const int N = 2010;
int f[N];
struct Good
{
int v, w;
};
int main()
{
int n, m; cin >> n >> m;
vector<Good> goods;
for(int i = 1; i <= n; i++)
{
int v, w, s; cin >> v >> w >> s;
// 二进制拆分物品
for(int k = 1; k <= s; k *= 2)
{
s -= k;
goods.push_back({v*k, w*k});
}
if(s>0) goods.push_back({v*s, w*s});
}
// 01背包
for(auto good: goods)
for(int j = m; j >= good.v; j--)
f[j] = max(f[j], f[j - good.v] + good.w);
cout << f[m] << endl;
return 0;
}
四、混合背包问题
要求:每种物品的个数分为多种情况。如下图所示:
\quad
其实这个题跟多重背包完全一样,无非就是要设置下每种物品个数上限,最朴素的,直接按照多重背包的不优化版写出程序如下:
#include <iostream>
using namespace std;
const int N = 1010;
int f[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, s; cin >> v >> w >> s;
if(s==-1) s = 1; // 设置物品数上限为1
else if(s==0) s = 1010; // 设置物品无限个,不超过背包体积
for(int j = m; j >= v; j--) //
for(int k = 0; k <= s && k * v <= j; k++)
f[j] = max(f[j], f[j - k * v] + k * w);
}
cout << f[m] << endl;
return 0;
}
\quad 利用二进制优化才能过这个题,如下:
#include <iostream>
#include <vector>
using namespace std;
const int N = 1010;
int f[N];
struct Good
{
int v, w;
};
int main()
{
vector<Good> goods;
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int v, w, s; cin >> v >> w >> s;
if(s==-1) s = 1;
else if(s==0) s = 1010;
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});
}
// 01背包
for(auto good: goods)
for(int j = m; j >= good.v; j--)
f[j] = max(f[j], f[j - good.v] + good.w);
cout << f[m] << endl;
return 0;
}
五、二维费用的背包问题
要求:一维背包问题可能只有一个限制,比如只有背包容量的限制;二维的话有两个限制,比如背包容量和物品总重量限制。来个题就明白啦:
样例:
输入:
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出:
8
思路:
\quad
f[i][j]
表示体积是i
,重量是j
时最大价值。第一重循环枚举物品,第二重循环枚举体积,第三重循环枚举重量。因为是01背包,故而都是从大到小枚举。程序如下:
#include <iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int main()
{
int n, V, M; cin >> n >> V >> M;
for(int i = 1; i <= n; i++)
{
int v, m, w; // 体积、重量、价值
cin >> 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);
}
cout << f[V][M] << endl;
return 0;
}
六、分组背包问题
要求:把物品分成若干组,每组里面最多选一件,来个题目:
样例:
输入:
3 5
2
1 2
2 4
1
3 4
1
4 5
输出:
8
思路:
\quad
f[j]
表示在j
体积下的最大价值。与01背包类似,第一重循环物品,第二重循环从大到小循环体积,然后依次把该组的每一个物品要么放进去要么不放进去。假设每一组有s
个物品,则
for(int i = 1; i <= n; i++)
for(int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v[0]]+w[0],..., f[j - v[s-1]]+w[s-1])
程序:
#include <iostream>
using namespace std;
const int N = 110;
int f[N], v[N], w[N];
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++)
{
int s; cin >> s;
for(int k = 1; k <= s; k++) cin >> v[k] >> w[k];
for(int j = m; j >= 0; j--)
for(int k = 1; k <= s; k++)
if(j >= v[k]) f[j] = max(f[j], f[j - v[k]] + w[k]);
}
cout << f[m] << endl;
return 0;
}
更好理解的版本
#include <iostream>
using namespace std;
const int N = 110;
int f[N][N], v[N], w[N];
// 参考多重背包,每个背包最多可以被用si次,这里相当于每组背包最多用1次
// f[i][j] 表示只用前i组物品和体积为j的情况下最多能装下多少价值的物品
// f[i][j] = max(f[i - 1][j], f[i - 1][j - 第i组的某个物品] + 该物品的价值)
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i ++ ){
int num; cin >> num;
for(int j = 0; j < num; j ++ ) cin >> v[j] >> w[j];
for(int j = 0; j <= m; j ++ ){
f[i][j] = f[i - 1][j];
for(int k = 0; k < num; k ++ )
if(j >= v[k])
f[i][j] = max(f[i][j], f[i - 1][j - v[k]] + w[k]);
}
}
int res = 0;
for(int i = 1; i <= n; i ++ ) res = max(res, f[i][m]);
cout << res << endl;
return 0;
}
七、背包问题求方案数
输入样例:
4 5
1 2
2 4
3 4
4 6
输出样例:
2
思路:
\quad
求方案数的时候就不能再像之前定义f[j]
表示体积不超过j
的情况下最大价值,而应该表示为体积恰好为j
的情况下的最大价值,我们可以在初始化的时候将f[j]
除了f[0]=0
外其他全部初始化为负无穷。同时还需要新增加一个数组g[j]
,表示体积恰好为j
的情况下得到最大价值的方案数。g[j]
更新方案如下所述:
g[0]=1
f[j]=max(f[j], f[j-v[i]]+w[i])
,当f[j]
由f[j]
转移而来时更新g[j]+=g[j]
,当其由f[j-v[i]]+w[i]
转移而来时更新g[j]+=g[j-v[i]]
,当二者相同时g[j]+=g[j]+g[j-v[i]]
- 求背包实现最大价值时的方案数,自然先求出背包所装物品的最大价值
maxW
,再求出用了多大的体积j
实现了最大价值,最后答案就是这些实现最大价值的体积下对应的方案数之和
#include <iostream>
using namespace std;
const int N = 1010;
const int mod = 1e9+7, INF = 1e9;
int f[N]; // f[N]表示恰好体积是j的情况下最大价值
int g[N]; // g[N]表示体积是j的情况下最大方案数
int main()
{
int n, m; cin >> n >> m;
g[0] = 1; // 初始方案数赋值
for(int i = 1; i <= m; i++) f[i] = -INF; // 赋值为负无穷才能得到体积恰好为j时的最大价值
for(int i = 1; i <= n; i++)
{
int v, w; cin >> v >> w;
for(int j = m; j >= v; j--)
{
int t = max(f[j], f[j-v]+w);
int s = 0; // 记录当前体积下方案数
if(t==f[j]) s += g[j]; // 是从体积j转移而来
if(t==f[j-v]+w) s += g[j-v]; // 是从体积j-v转移而来
s = s % mod;
f[j] = t, g[j] = s;
}
}
int maxW = 0; // 搜素最大价值
for(int i = 0; i <= m; i++) maxW = max(maxW, f[i]);
int res = 0; // 记录总的方案数
for(int i = 0; i <= m; i++)
{
if(maxW == f[i]) // 当前体积下得到最大价值
{
res += g[i];
res %= mod;
}
}
cout << res << endl;
return 0;
}
八、最优物品选择方案
输入样例:
4 5
1 2
2 4
3 4
4 6
输出样例:
2
思路:见博客
九、有依赖的背包问题
要求:选一件物品前必须选其依赖的物品