背包问题作为基本的动态规划问题很有必要掌握,最近看了《背包九讲》,自己总结了下:
01背包
问题: 总体积为V的背包,存在n个物品,第
i
i
i个物品的体积为
v
i
v_i
vi,价值为
w
i
w_i
wi,求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大,输出最大价值。
动态规划总体来说就是对一个元素选和不选的问题,而在背包问题中,对于每种背包问题,都存在一个状态转移方程
d
p
[
i
]
[
j
]
=
dp[i][j]=
dp[i][j]=max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
i
]
+
w
i
)
(dp[i-1][j],dp[i-1][j-v_i]+w_i)
(dp[i−1][j],dp[i−1][j−vi]+wi)
这里
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]就代表的是前i个物品放入j空间下的最大价值,因为
i
i
i总是从
i
−
1
i-1
i−1转移过来的,所以当遍历到 第i个物品时,我们可以选第
i
i
i个物品,则得到
d
p
[
i
−
1
]
[
j
−
v
i
]
+
w
i
dp[i-1][j-v_i]+w_i
dp[i−1][j−vi]+wi,不选则是
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j] ,求这两个等式的最大值就是我们要求的
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]。
所以,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]要么等于
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j],要么等于
d
p
[
i
−
1
]
[
j
−
v
i
]
+
w
i
dp[i-1][j-v_i]+w_i
dp[i−1][j−vi]+wi,对于动态规划的初学者,最好把表格画上,这里我们只给出v[1]那行的转移路线,因为v[1]=4,所以当 j<4时,直接
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
dp[i][j]=dp[i-1][j]
dp[i][j]=dp[i−1][j],当j>4时,
d
p
[
i
]
[
j
]
=
dp[i][j]=
dp[i][j]=max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
i
]
+
w
i
)
=
d
p
[
i
−
1
]
[
j
−
v
i
]
+
w
i
(dp[i-1][j],dp[i-1][j-v_i]+w_i)=dp[i-1][j-v_i]+w_i
(dp[i−1][j],dp[i−1][j−vi]+wi)=dp[i−1][j−vi]+wi,v[2]所在行同理。
我们先假设
V
=
7
,
n
=
3
,
v
=
[
2
,
4
,
3
]
,
w
=
[
5
,
6
,
7
]
V=7,n=3,v=[2,4,3],w=[5,6,7]
V=7,n=3,v=[2,4,3],w=[5,6,7]得到
const int n=1010;
int dp[n][n];//这里我们是故意开大,也可以定义int dp[v.size()][V]
int zero_one_pack(vector<int>&v, vector<int>&w, int V)
{
dp[0][0] = 0;//一般动态规划都需要初始化,这里我们dp是全局的,默认元素为0
for (int i = 1; i <=v.size(); i++)
{
for (int j = 0; j <= V; j++)
{
dp[i][j] = max(dp[i-1][j], dp[i - 1][j - v[i]] + w[i]);
}
}
return dp[v.size()][V];
}
时间复杂度
O
(
n
V
)
O(nV)
O(nV),空间复杂度
O
(
n
V
)
O(nV)
O(nV)
我们可以对上式进行的空间复杂度进行优化,优化为
O
(
V
)
O(V)
O(V),这里利用了滚动数组的原理,从图我们可以得到,第i行只与第i-1行有关,则可以将其优化为
O
(
2
V
)
O(2V)
O(2V),继续观察得到,当
v
[
i
]
>
0
v[i]>0
v[i]>0时,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]只与
d
p
[
i
−
1
]
[
j
−
v
[
i
]
]
dp[i-1][j-v[i]]
dp[i−1][j−v[i]]和
d
p
[
i
−
1
]
[
j
]
]
dp[i-1][j]]
dp[i−1][j]]有关,即只与
d
p
[
i
−
1
]
dp[i-1]
dp[i−1]前
j
j
j个元素有关,则我们怎么能只利用一维数组实现呢?为什么不能正向遍历只能反向遍历?
我们发现,第j列的元素只与第j-v[i]列的元素有关,当正向遍历到
d
p
[
j
]
dp[j]
dp[j]时,只要
j
−
v
[
i
]
>
=
0
j-v[i]>=0
j−v[i]>=0,
d
p
[
j
−
v
[
i
]
]
dp[j-v[i]]
dp[j−v[i]]一定发生了改变。所以考虑反向遍历,我们发现正好
d
p
[
j
]
dp[j]
dp[j]可以利用没有改变的
d
p
[
j
−
v
[
i
]
]
dp[j-v[i]]
dp[j−v[i]],优化代码如下
const int n=1010;
int dp[n];
int Op_zero_one_pack(vector<int>&v, vector<int>&w, int V)
{
dp[0] = 0;
for (int i = 0; i < v.size(); i++)
{
for (int j = V; j >= v[i]; j--)
{
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
return dp[V];
}
空间复杂度
O
(
V
)
O(V)
O(V)
这里还有一个问题,当题目要求必须把背包装满时,我们就加一个小改动,初始化时将dp数组所有元素初始化为
−
1
-1
−1,
d
p
[
0
]
=
0
dp[0]=0
dp[0]=0。再加上一个判断
i
f
(
d
p
[
j
−
v
[
i
]
]
!
=
−
1
)
if(dp[j-v[i]]!=-1)
if(dp[j−v[i]]!=−1),即
j
−
v
[
i
]
j-v[i]
j−v[i]的空间放满是才会传递到
d
p
[
j
]
dp[j]
dp[j]。代码如下:
const int n=1010;
int dp[n];
int Full_zero_one_pack(vector<int>&v, vector<int>&w, int V)
{
memset(dp,-1,sizeof(dp));//初始化为-1
dp[0] = 0;
for (int i = 0; i < v.size(); i++)
{
for (int j = V; j >= V[i]; j--)
{
if(dp[j-v[i]]!=-1)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
return dp[V];
}
空间复杂度 O ( V ) O(V) O(V)
2 完全背包
完全背包:题目要求基本上和01背包一样,唯一的区别在于完全背包中规定每种物品的数目为无限。同时,有优化和不优化两种版本。
当不优化时,可以将其视为01背包,仅仅是比01背包多了一层循环。
01背包是求
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
v
[
i
]
]
+
w
[
i
]
)
dp[j] = max(dp[j], dp[j - v[i]] + w[i])
dp[j]=max(dp[j],dp[j−v[i]]+w[i]),当是完全背包,求
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
v
[
i
]
]
+
w
[
i
]
,
d
p
[
j
−
2
v
[
i
]
]
+
2
w
[
i
]
)
,
d
p
[
j
−
3
v
[
i
]
]
+
3
w
[
i
]
.
.
.
.
.
d
p
[
j
−
k
v
[
i
]
]
+
k
w
[
i
]
dp[j] = max(dp[j], dp[j - v[i]] + w[i],dp[j - 2v[i]] + 2w[i]),dp[j - 3v[i]] + 3w[i] ..... dp[j - kv[i]] + kw[i]
dp[j]=max(dp[j],dp[j−v[i]]+w[i],dp[j−2v[i]]+2w[i]),dp[j−3v[i]]+3w[i].....dp[j−kv[i]]+kw[i],
k
<
=
V
/
c
i
k<=V/c_i
k<=V/ci,这里给出二维版本,一维版本类似:
const int n=1010;
int dp[n][n];//这里我们是故意开大,也可以定义int dp[v.size()][V]
int Complete_pack(vector<int>&v, vector<int>&w, int V)
{
dp[0][0] = 0;//一般动态规划都需要初始化,这里我们dp是全局的,默认元素为0
for (int i = 1; i <= v.size(); i++)
{
for (int j = 1; j <= V; j++)
{
for(int k=1;k<=j/v[i];k++)
if(j>=v[i-1])
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j -k* v[i-1]] + k*w[i-1]);
}
}
return dp[v.size()][V];
}
时间复杂度
O
(
n
V
k
)
O(nVk)
O(nVk),空间复杂度为
O
(
n
V
)
O(nV)
O(nV).。
优化版本和01背包的优化版本类似,就是01背包极力要避免的事情(
d
p
[
j
]
dp[j]
dp[j]使用了已经改变的
d
p
[
j
−
v
[
i
]
]
dp[j-v[i]]
dp[j−v[i]]),正是完全背包所想要的。大家可以画个图就知道了。从小到大遍历
const int n=1010;
int dp[n];//这里我们是故意开大,也可以定义int dp[v.size()][V]
int OP_Complete_pack(vector<int>&v, vector<int>&w, int V)
{
dp[0] = 0;//一般动态规划都需要初始化,这里我们dp是全局的,默认元素为0
for (int i = 0; i < v.size(); i++)
{
for (int j = v[i]; j <= V; j++)
{
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
return dp[V];
}
时间复杂度 O ( n V ) O(nV) O(nV),空间复杂度为 O ( V ) O(V) O(V).。