之前我们介绍了如何用动态规划算法求解最长公共子序列问题,本文继续介绍如何用动态规划算法求解0-1背包问题,并使用C++进行代码实现。
1. 0-1背包问题
给定 n n n种物品和一背包。物品 i i i的体积是 w i w_i wi,其价值为 v i v_i vi,背包的容量为 C C C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?这个问题就是0-1背包问题,在运筹学中这属于一个特殊的整数规划问题, K n a p ( 1 , n , C ) Knap(1, n, C) Knap(1,n,C)定义如下:
max ∑ i = 1 n v i x i v i > 0 \begin{matrix}\max{\sum_{i=1}^{n}v_ix_i} & v_i > 0\end{matrix} max∑i=1nvixivi>0
{ ∑ i = 1 n w i x i ≤ C w i > 0 x i ∈ { 0 , 1 } 1 ≤ i ≤ n \left\{\begin{matrix} \sum_{i=1}^{n}w_ix_i \leq C & w_i > 0 \\ x_i \in \{0,1\} & 1 \leq i \leq n \end{matrix}\right. {∑i=1nwixi≤Cxi∈{0,1}wi>01≤i≤n
例如: w = ( w 1 , w 2 , w 3 ) = ( 2 , 3 , 4 ) w=(w_1, w_2, w_3)=(2, 3, 4) w=(w1,w2,w3)=(2,3,4), v = ( v 1 , v 2 , v 3 ) = ( 1 , 2 , 5 ) v=(v_1, v_2, v_3)=(1, 2, 5) v=(v1,v2,v3)=(1,2,5),求 K n a p ( 1 , 3 , 6 ) Knap(1, 3, 6) Knap(1,3,6)。
- 取 x = ( 1 , 0 , 1 ) x = (1, 0, 1) x=(1,0,1)时, K n a p ( 1 , 3 , 6 ) = ( v 1 x 1 + v 2 x 2 + v 3 x 3 ) = 1 × 1 + 2 × 0 + 5 × 1 = 6 Knap(1, 3, 6) = (v_1x_1+v_2x_2+v_3x_3)=1 \times 1 + 2 \times 0 + 5 \times 1 = 6 Knap(1,3,6)=(v1x1+v2x2+v3x3)=1×1+2×0+5×1=6最大。
如果用穷举法求解,时间复杂度为 O ( n 2 n ) O(n2^n) O(n2n)。下面我们使用动态规划算法来解决这个问题。
2. 动态规划算法思路
2.1 0-1背包问题的子问题
设所给0-1背包问题的子问题记为
K
n
a
p
(
i
,
n
,
j
)
Knap(i, n, j)
Knap(i,n,j),
j
≤
C
j \leq C
j≤C (假设
C
C
C,
w
i
w_i
wi取整数),其定义为:
max
∑
k
=
i
n
v
k
x
k
\max{\sum_{k=i}^{n}v_kx_k}
max∑k=invkxk
{ ∑ k = i n w k x k ≤ j x k ∈ { 0 , 1 } , i ≤ k ≤ n \left\{\begin{matrix} \sum_{k=i}^{n}w_kx_k \leq j \\ x_k \in \{0,1\}, i \leq k \leq n \end{matrix}\right. {∑k=inwkxk≤jxk∈{0,1},i≤k≤n
其中,子问题的背包容量
j
j
j在不断变化。令m(i, j)
代表第
i
…
n
i…n
i…n个物体在背包容量为
j
j
j时的最大价值,可以推出如下递归式。
2.2 最优值的递归式
m ( i , j ) = { max { m ( i + 1 , j ) , m ( i + 1 , j − w i ) + v i } j ≥ w i m ( i + 1 , j ) 0 ≤ j < w i m(i, j)=\left\{\begin{matrix} \max \{m(i + 1, j), m(i + 1, j - w_i) +v_i\} & j \geq w_i \\ m(i+1, j) & 0 \leq j < w_i \end{matrix}\right. m(i,j)={max{m(i+1,j),m(i+1,j−wi)+vi}m(i+1,j)j≥wi0≤j<wi
说明:
- 当 j < w i j<w_i j<wi时,只有 x i = 0 x_i=0 xi=0,所以 m ( i , j ) = m ( i + 1 , j ) m(i,j)=m(i+1, j) m(i,j)=m(i+1,j);
- 当 j ≥ w i j \geq w_i j≥wi时, { 取 x i = 0 时, 为 m ( i + 1 , j ) 取 x i = 1 时, 为 m ( i + 1 , j − w i ) + v i \left\{\begin{matrix} 取x_i=0时, & 为m(i+1, j) \\ 取x_i=1时, & 为m(i + 1, j - w_i) +v_i \end{matrix}\right. {取xi=0时,取xi=1时,为m(i+1,j)为m(i+1,j−wi)+vi
临界条件:
- m ( n , j ) = { v n j ≥ w n 0 0 ≤ j < w n m(n, j)=\left\{\begin{matrix} v_n & j \geq w_n \\ 0 & 0 \leq j < w_n \end{matrix}\right. m(n,j)={vn0j≥wn0≤j<wn
从 m ( i , j ) m(i, j) m(i,j)的递归式容易看出,动态规划算法需要 O ( n C ) O(nC) O(nC)计算时间。
3. 程序代码
给定背包容量为55,物体数量为14,物品体积:{3, 5, 11, 7, 9, 2, 13, 17, 26, 24, 19, 14, 12, 6},物品价值:{0, 5, 7, 15, 8, 9, 1, 10, 17, 30, 26, 20, 17, 9, 6}。以下C++代码实现了动态规划算法求解0-1背包问题。
//动态规划法求解0-1背包问题
#include <iostream>
using namespace std;
int max(int a, int b) { //求a,b元素中的最大值
return (a > b) ? a : b;
}
int min(int a, int b) { //求a,b元素中的最小值
return (a < b) ? a : b;
}
//求背包中物体总价值的最大值
void knapsack(int itemSize[], int itemValue[], int capacity, int itemNumber, int **m) {
//物体数量为itemNumber,物体i的体积为itemsize[i],价值为itemValue[i],背包容量为capacity
//m[itemNumber+1][capacity+1]存储动态规划表
//m[i][j]表示第i..itemNumber个物体在背包容量为j时背包所能装入的最大价值
int jMax = min(itemSize[itemNumber] - 1, capacity);
int j;
for (j = 0; j <= jMax; j++) {
m[itemNumber][j] = 0;
}
for (j = itemSize[itemNumber]; j <= capacity; j++) {
m[itemNumber][j] = itemValue[itemNumber];
}
int i;
for (i = itemNumber - 1; i >= 2; i--) { //i>1,表示对i=1暂时不处理,i=1时只需求m[1][capacity]
jMax = min(itemSize[i] - 1, capacity);
for (j = 0; j <= jMax; j++) {
m[i][j] = m[i + 1][j];
}
for (j = itemSize[i]; j <= capacity; j++) {
m[i][j] = max(m[i + 1][j], m[i + 1][j - itemSize[i]] + itemValue[i]);
}
}
if (capacity >= itemSize[1]) {
m[1][capacity] = max(m[2][capacity], m[2][capacity - itemSize[1]] + itemValue[1]);
}
else {
m[1][capacity] = m[2][capacity];
}
}
//回溯输出解x[1..itemNumber]
void traceback(int itemSize[], int capacity, int itemNumber, int **m, int *x) {
int i;
for (i = 0; i < itemNumber; i++) {
//在背包容量为capacity时,考虑有第i..itemNumber个物体和有第i+1..itemNumber个物体
//如果二者的最大价值相同,则说明第i个物体未放入背包。
if (m[i][capacity] == m[i+1][capacity]) {
x[i] = 0;
}
else { //否则说明第i个物体放入背包
x[i] = 1;
capacity -= itemSize[i]; //背包容量减去第i个物体的体积
}
}
x[itemNumber] = (m[itemNumber][capacity]) ? 1 : 0;
}
int main() {
int capacity = 55;
int itemNumber = 14;
int itemSize[] = {0, 3, 5, 11, 7, 9, 2, 13, 17, 26, 24, 19, 14, 12, 6};
int itemValue[] = {0, 5, 7, 15, 8, 9, 1, 10, 17, 30, 26, 20, 17, 9, 6};
//为动态规划表m,解向量x开空间
int **m = new int*[itemNumber + 1];
int i;
for (i = 0; i < itemNumber + 1; i++) {
m[i] = new int[capacity + 1];
}
int *x = new int[itemNumber + 1];
knapsack(itemSize, itemValue, capacity, itemNumber, m);
traceback(itemSize, capacity, itemNumber, m, x);
for (i = 1; i <= itemNumber; i++) { //输出解x[1..itemNumber],一个01序列,0代表没选,1代表选中
cout << x[i] << " ";
if (i == itemNumber) {
cout << endl;
}
}
for (i = 1; i <= itemNumber; i++) { //输出装入背包的物体编号
if (x[i] == 1) {
cout << "Item " << i << " is chosen, ";
cout << "its value is " << itemValue[i] << ", its size is " << itemSize[i] << endl;
}
}
cout << "The optimal solution (the largest value of the knapsack) is ";
cout << m[1][capacity] << endl; //输出最优解,即背包中物体总价值的最大值
//释放动态规划表m,解向量x占据的空间
for (i = 0; i < itemNumber + 1; i++) {
delete[]m[i];
}
delete[]m;
delete[]x;
return 0;
}
4. 运行结果
运行上述代码,程序会输出表示物体是否被选中的01向量序列,并输出最优解的值(即背包的最大价值),如下图所示。