动态规划解决0-1背包问题
问题引入:
话说,有一天,叶天帝打算带领天庭众人去扫平禁地,斩杀发起黑暗动乱的至尊,毫无疑问,这将是一场恶战,叶天帝当然要好好准备一翻。
叶天帝就找来了段德,索要其宝物,段德迫于众人威胁,就忍痛拿出了珍藏许久的宝贝,叶天帝十分高兴,准备都装入空间戒指,出发征战禁地。这时,一旁的大黑狗突然嘿嘿怪笑,叶天帝猛然抬头,想到了什么,原来,叶天帝最大的空间戒指被大黑狗叼走了。众人一阵无语,大家都知道,只要进了大黑狗的嘴,就休想再拿回来,叶天帝无奈,只能在一个很小的空间戒指中装征战禁地的资源,但是空间有限,战争又很残酷,叶天帝就想在有限的空间内装下最大价值的资源。
叶天帝找众人商量,如何能使得有限的空间戒指装下最大价值的资源呢?众人皆挠头。叶天帝无奈,只能自创功法。在闭关九九八十一日之后,叶天帝出关,顿时世间霞光万道,困扰众人的问题已被破解,叶天帝将这一创世功法命名为 0-1背包大法。
这时,一旁的大黑狗突然贼溜溜盯着叶天帝,突然开口:“叶小子的脑袋怎么闪我眼睛呢?”。叶天帝顿时暴怒,想要烤了大黑狗…………
什么是 0-1 背包问题?
问题:
给定 N个物品和一个背包。第
i
i
i 个物品的重量为
W
i
W_i
Wi ,其价值为
V
i
V_i
Vi,背包的总容量为
C
C
C。如何选取物品装入背包,使得背包中所装入的物品的总价值最大?
需要注意的是,0-1 背包问题在选择装入背包的物品时,对于物品
i
i
i,只有两种选择,要么装入背包,要么不装入背包。不能将物品
i
i
i 装入多次,也不能只装入物品
i
i
i 的一部分。
【解题思路】:
我们要用动态规划来解决 0-1 背包问题,可以先使用递归的思想,看能不能得到一个递推方程,然后根据这个递推方程再进行时间和空间上的优化。
在 0-1 背包问题中有 N 个物品,我们设定一个数组
W
[
i
.
.
.
N
−
1
]
W[i...N-1]
W[i...N−1] 来表示物品
i
i
i 的体积,数组
V
[
i
.
.
.
N
−
1
]
V[i...N-1]
V[i...N−1] 来表示物品
i
i
i 的价值,再创建一个二维数组
D
P
[
N
]
[
M
]
DP[N][M]
DP[N][M],N 为物品的个数,M 为背包当前的容量,表示当前的最大价值。。例:
D
P
[
i
]
[
j
]
DP[i][j]
DP[i][j] 表示取前
i
i
i 个物品,使其体积不超过
j
j
j 的最优解,即:最大价值。
写出递推方程:
D
P
[
k
]
[
w
]
=
{
D
P
[
k
−
1
]
[
w
]
第k件太重,超过背包容量
m
a
x
{
D
P
[
k
−
1
]
[
w
]
,
D
P
[
k
−
1
]
[
w
−
W
[
k
]
]
+
V
[
k
]
}
选和选中取较大者
DP[k][w] = \begin{cases} DP[k-1][w] & \text{第k件太重,超过背包容量} \\ max\{DP[k-1][w],DP[k-1][w-W[k]]+V[k]\} & \text{选和选中取较大者} \end{cases}
DP[k][w]={DP[k−1][w]max{DP[k−1][w],DP[k−1][w−W[k]]+V[k]}第k件太重,超过背包容量选和选中取较大者
现给出背包容量
C
C
C = 20,物品的体积和价值如下:
序号 | 物品体积 ( W [ i ] W[i] W[i]) | 物品价值 ( V [ i ] V[i] V[i]) |
---|---|---|
0 | 2 | 3 |
1 | 3 | 4 |
2 | 4 | 5 |
3 | 5 | 8 |
4 | 9 | 10 |
写出递归树:(画的多了容易混,就只写出来部分)
- 我们依然采用我们的 选 和 不选 大法
什么?你不知道这个功法?请点击下面链接进行升级打怪
- 分析:
(1)选,表示选择该物品,然后在前 k − 1 k-1 k−1 个物品中继续选,背包容量则应减去该物品的体积,总价值就加上该物品的价值。
(2)不选:表示不选择该物品,然后在前 k − 1 k-1 k−1 个物品中继续选择,背包容量和价值不变。
(3)在选和不选之间取较大者。
(4)还有一种情况就是:当前物品的体积已经超过了当前的背包容量,则直接跳过,在前面 k − 1 k-1 k−1 个物品中继续选择。
填写出DP数组,即:
D
P
[
i
]
[
j
]
DP[i][j]
DP[i][j] 表示取前
i
i
i 个物品,使其体积不超过
j
j
j 的情况下,背包中的最大价值。
根据上面的分析,我们可以写出递推程序:
#include<iostream>
using namespace std;
const int N = 6;//物品数量
const int C = 20;//背包容量
int DP[N][C+1]={0,0};//最大价值
int W[N] = { 0,2,3,4,5,9 };//物品的体积
int V[N] = { 0,3,4,5,8,10 };//物品的价值
int X[N];//构造最优解
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
void KnapSack()
{
int k, w;//第k个商品,j为当前容量
int i, j;
for (k = 1; k < N; k++)
{
for (w = C; w>=0; w--)
{
if (W[k] > w)
DP[k][w] = DP[k - 1][w];
else
{
int a = DP[k - 1][w - W[k]] + V[k];
int b = DP[k - 1][w];
DP[k][w] = max(a, b);
}
}
}
}
void Traceback() //选择了哪些物品
{
int c = C, n = N;
for (int i = n; i >=0; i--)
{
if (DP[i][c] == DP[i - 1][c])
X[i] = 0;
else
{
X[i] = 1;
c = c - W[i];
}
}
X[n] = (DP[1][c])==0 ? 1 : 0;
}
int main()
{
KnapSack();
int i, j;
Traceback();
for (i = 0; i < N; i++)
{
if (X[i] == 1)
{
cout << "选择第" << i << "件物品:";
cout << "体积:" << W[i] << " " << "价值:" << V[i] << endl;
}
}
cout <<"\n"<<"最大价值为:"<< DP[N-1][C] << endl;
return 0;
}
输出:
选择第1件物品:体积:2 价值:3
选择第3件物品:体积:4 价值:5
选择第4件物品:体积:5 价值:8
选择第5件物品:体积:9 价值:10
最大价值为:26
【注】:
我们设置物品时,第一个要设置为一个假想物品,即:它的体积为0,价值为0。为什么要这么设置呢?难道是玄学的问题?No,No,No。
因为我们下面递推时,要从第一个物品开始(下标从0开始),因为如果从 0 开始的话,int a = DP[k - 1][w - W[k]] + V[k];
,这一句就会发生下标是负数的情况。为了避免这种情况,我们就设置一个假想的物品,体积和价值都为0,它只起到一个占位的作用。
但是呢,这个算法用到了一个二维数组,嗯?二维……如果物品很多呢,如果背包很大呢?是不是就得开辟一个灰常大的二维数组,显然,这不是我们想要的。
优化空间的 0-1背包
我们分析以上代码,发现一个惊天大秘密,每次计算最大价值时,它只和它的正上方元素和左上方元素相关,而且每次计算完成之后,该位的值就会更新,所以,我们是不是可以用一个一维数组来代替这个二维数组呢?
我们设定一个一维数组 dp[C+1],为什么是数组大小是 C+1 呢?因为下标从0开始,dp[C] 才是我们要的结果。
dp[j] 就表示背包容量为
j
j
j 时,所获得的最大价值。
#include<iostream>
using namespace std;
const int N = 6;//物品数量
const int C = 20;//背包容量
int dp[C + 1] = { 0};//最大价值
int W[N] = { 0,2,3,4,5,9 };//物品的体积
int V[N] = { 0,3,4,5,8,10 };//物品的价值
int max(int a, int b)
{
int m = a > b ? a : b;
return m;
}
void KnapSack()
{
int k, w;//第k个商品,j为当前容量
int i, j;
for (i = 0; i < N; i++)
{
for (j = C; j >= W[i]; j--)
{
dp[j] = max(dp[j], dp[j - W[i]] + V[i]);
}
}
}
int main()
{
KnapSack();
int i, j;
cout << "最大价值为:" << dp[C] << endl;
return 0;
}
至此,我们可以采用一维数组来解决 0-1 背包问题。
叶天帝也带领众人去征战禁地……
战况如何?
请点击下面链接,叶天帝将使用分治大法,逐一击败众位黑暗至尊