01背包问题
动态规划(DP)—— 算法设计方法之一。
使用递归求解
问题:有几个重量和价值分别为Wi和Vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和最大的值。
限制条件
1<=n<=100
1<=Wi,Vi<=100
1<=W<=10000
输入样例:
n = 4
(w ,v) = { (2,3), (1,2),(3,4), (2,2) }
W=5;
输出样例:
7(选择0,1,3号)
这种问题就是背包问题。背包问题看起来非常复杂,需要测试很多种组合。首先我们对每个物品是否放入背包进行搜索试试看。
代码如下:
#include <iostream>
#define MAX_N 100
using namespace std;
int n, W; //n个 物品, 总重量不超过W
int w[MAX_N], v[MAX_N];
//从第i个物品开始挑选总重量小于j的部分
int rec(int i, int j) {
int res;
if (i == n) {
//已经没有剩下的了
return res = 0;
} else if (j < w[i]) {
res = rec(i + 1, j);//这个物品超重了,尝试下一个
} else {
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]); //在这里进行分支前一个是不包含第i个,后一个是包含第i个。
}
return res;
}
void init() {
cin >> n ;
for (int i = 0; i < n; i++) {
cin >> w[i];
cin >> v[i];
}
cin>>W;
}
int main() {
init();
cout << rec(0, W) << endl;
return 0;
}
改进递归方法
虽然上述方法可以求解,但是显然这种方法不是很好。它的搜索深度为n,最坏情况需要 O ( 2 n ) O(2^n) O(2n)时间复杂度。该递归调用方法使用了遍历二叉树搜索的原理。
其实这里是有改进的地方,观察二叉树会发现rec(3,2)执行了两次,但是如果我们在执行第一次的时候将rec(3,2)的值保存起来,那么下次执行时就可以直接调用结果了(这便是记忆化搜索)。
来试试新的方法:增加一个二维数组dp[][],将执行结果没一步保存在其中。
代码如下:
#include <iostream>
#define MAX_N 100
using namespace std;
int n, W; //n个 物品, 总重量不超过W
int w[MAX_N], v[MAX_N];
int dp[MAX_N][MAX_N];
//从第i个物品开始挑选总重量小于j的部分
int rec(int i, int j) {
if (dp[i][j] != 0) {
return dp[i][j]; //如果有记录则直接返回结果
}
int res;
if (i == n) {
//已经没有剩下的了
return res = 0;
} else if (j < w[i]) {
res = rec(i + 1, j);//这个物品超重了,尝试下一个
} else {
res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]); //在这里进行分支前一个是不包含第i个,后一个是包含第i个。
}
dp[i][j] = res; //结果保存
return res;
}
void init() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> w[i];
cin >> v[i];
}
cin >> W;
}
int main() {
init();
cout << rec(0, W) << endl;
return 0;
}
推导递推公式使用dp
仔细研究前面的算法用到的这个记忆数组。记dp[i][j]为根据rec的定义,从第i个物品开始挑选总重量小于j时,总价值最大的值。于是我们有一下递推公式。
d
p
[
n
]
[
j
]
=
0
dp[n][j]=0
dp[n][j]=0
d
p
[
i
]
[
j
]
=
{
d
p
[
i
+
1
]
[
j
]
(
j
<
w
[
i
]
)
m
a
x
(
d
p
[
i
+
1
]
[
j
]
,
d
p
[
i
+
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
dp[i][j]=\left\{ \begin{aligned} & dp[i+1][j] (j<w[i]) \\ & max( dp[i+1][j], dp[i+1][j-w[i]]+v[i]) \end{aligned} \right.
dp[i][j]={dp[i+1][j](j<w[i])max(dp[i+1][j],dp[i+1][j−w[i]]+v[i])
不用递归函数,直接使用地推公式将各项值计算出来,然后用二重信息即可解决该问题。
int dp[MAX_N+1][MAX_N+1] {}; //初始化为全0
void solve2(){
for(int i=n-1;i>=0;i--){
for(int j=0;j<=W;j++){
if(j<w[i]){
dp[i][j]=dp[i+1][j];
}else{
dp[i][j]=max(dp[i+1][j] , dp[i+1][j-w[i]] + v[i]);
}
}
}
}
虽然这个函数的时间复杂度与前一个相同
O
(
n
×
W
)
O(n×W)
O(n×W) ,但是简明了许多。
动态规划问题(dp)可以分析其递推公式。
注意:全局数组和静态数组会被初始化为0;局部数据需要手动初始化为0,例如:int a[4]={} ; 或 int a[4] {} ; 或 int a[4] {0} 。如果括号里写0或什么都不写将会把数组全部初始化为0,但是如果这样写:int a[4] {1}; ,将会被初始化为1 0 0 0.
其他推导方法
递推公式有多种推导方法,使用不同的递推公式我们可以得到多种算法。
正向推导
刚讲到DP中关于i的循环是逆向进行的。如下递推公式是正向进行的。
d
p
[
i
+
1
]
[
j
]
:
=
dp[i+1][j]:=
dp[i+1][j]:=从前i个物品中挑选出总重量不超过j的物品时,总价值的最大值
d
p
[
0
]
[
j
]
=
0
dp[0][j]=0
dp[0][j]=0
d
p
[
i
+
1
]
[
j
]
=
{
d
p
[
i
]
[
j
]
,
(
j
<
w
[
i
]
)
m
a
x
(
d
p
[
i
]
[
j
]
,
d
p
[
i
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
dp[i+1][j]=\left\{ \begin{aligned} dp[i][j] ,(j<w[i]) \\ max( dp[i][j], dp[i][j-w[i]]+v[i]) \end{aligned} \right.
dp[i+1][j]={dp[i][j],(j<w[i])max(dp[i][j],dp[i][j−w[i]]+v[i])
仔细观察公式会发现,dp中的i和w和v中的i不同,dp中的i表示前i个物品,而w和v中的i表示物品的编号,即编号是从0开始的。
void solve() {
for (int i = 0; i < n; i++) {
for (int j = 0; j <= W; j++) {
if (j < w[i]) {
dp[i + 1][j] = dp[i][j];
} else {
dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
}
}
cout<<dp[n][W];
}
状态转移法
除了用递推方式逐项求解外,还可以把状态转换想象成从“前i个物品中挑选出总重量不超过j时的状态” 向“前i+1个物品中选取总重量不超过j“ 和 ”前i+1个物品中选取总重量不超过j+w[i] 时的状态“的转移,于是可以实现如下形式。
void solve2() {
for (int i = 0; i < n; i++) {
for (int j = 0; j < W; j++) {
dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]);
if (j + w[i] <= W) {
dp[i + 1][j + w[i]] = max(dp[i + 1][j + w[i]], dp[i][j] + v[i]);// dp[i+1][j+w[i]]表示前i+1个物品,重量不超过j+w[i]的价值,dp[i][j]+v[i],表前i个物品重量不超过j的价值,加上第i+1个物品的价值,也就是说,它和dp[i + 1][j + w[i]]相比默认选择了第i+1个物品。
}
}
}
cout << dp[n][W];
上述问题中,从当前状态转移到下一状态的形式,需要注意初项之外也需要初始化(在本问题中,因为价值的初始值为0,所以没有显示的初始化,在有些问题中初始值为无穷大等,需要显示的初始化。)
同一个问题可能有很多不同的解法:搜索记忆法、递推关系dp、状态转移dp等。根据具体的问题选择较好的方法。
01背包问题2
问题:有几个重量和价值分别为Wi和Vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和最大的值。
限制条件:
1 < = n < = 100 1 < = w i ⩽ 1 0 7 1 ⩽ v i ⩽ 100 1 < = W < = 1 0 9 1<=n<=100 \\ 1<=w_i \leqslant 10^7 \\ 1 \leqslant v_i \leqslant 100 \\ 1<=W<=10^9 \\ 1<=n<=1001<=wi⩽1071⩽vi⩽1001<=W<=109
输入样例:
n = 4
(w ,v) = { (2,3), (1,2),(3,4), (2,2) }
W=5;
输出样例:
7(选择0,1,3号)
它与文章开头的01背包问题的区别仅仅是限制条件的不同。求解问题的复杂度是 O ( n W ) O(nW) O(nW),显然现在的问题中w的范围非常大,如果继续使用前面的方法,那么dp数组将会非常大。
在之前的方法中用dp表示一定的重量下的最大价值,现在我们用dp表示一定价值下的最小重量。
定义:
d p [ i + 1 ] [ j ] : = 前 i 个 物 品 中 挑 选 出 价 值 总 和 为 j 时 总 重 量 最 小 值 ( 不 存 在 时 就 是 I N F ) dp[i+1][j] := 前i个物品中挑选出价值总和为j时总重量最小值(不存在时就是INF) dp[i+1][j]:=前i个物品中挑选出价值总和为j时总重量最小值(不存在时就是INF)
分析:
由于前0个物品没有重量,所以:
d
p
[
0
]
[
j
]
=
I
N
F
d
p
[
0
]
[
0
]
=
0
dp[0][j] = INF \\ dp[0][0] = 0
dp[0][j]=INFdp[0][0]=0
和前文同理可得递推关系式:
d p [ 0 ] [ 0 ] = 0 d p [ 0 ] [ j ] = I N F , j ! = 0 d p [ i + 1 ] [ j ] = { m i n ( d p [ i ] [ j ] , d p [ i + 1 ] [ j − v [ i ] ] + w [ i ] ) , j > = v [ i ] d p [ i ] [ j ] , j < v [ i ] \begin{aligned} &dp[0][0] = 0 \\ &dp[0][j] = INF , j != 0 \\ &dp[i+1][j] = \left \{ \begin{aligned} & min( dp[i][j], dp[i+1][j-v[i]] +w[i] ) , \qquad j>=v[i]\\ & dp[i][j] ,\qquad j<v[i] \end{aligned} \right. \end{aligned} dp[0][0]=0dp[0][j]=INF,j!=0dp[i+1][j]={min(dp[i][j],dp[i+1][j−v[i]]+w[i]),j>=v[i]dp[i][j],j<v[i]
问题最终的解为:
d p [ n ] [ j ] ⩽ W dp[n][j] \leqslant W dp[n][j]⩽W 的最大的 j j j
int dp[MAX_N+1][MAX_N * MAX_V+1]; //能够容纳的最大价值MAX_N * MAX_V
int solve(){
fill(dp[0],dp[0]+MAX_N * MAX_V +1 , INF);
dp[0][0] = 0;
for(int i =0; i< n ; i++){
for(int j =0; j<= MAX_N*MAX_V; j++){
if(j < v[i]){
dp[i+1][j] = dp[i][j];
else{
dp[i+1][j] = dp[i+1][j-v[i]] +w[i];
}
}
}
// 在最后一行找到dp[n][j]<=W 时的j
res = 0;
for(int i = 0; i<= MAX_N*MAX_V;i++)
if(dp[n][i] <= W){
res =i-1;
break;
}
return res;
}