===== 本文算法的时空复杂度都未达到最优,核心目的在于展现并理解动态规划的算法过程。=====
0-1背包问题
给定 n n n 种物品和一个背包。物品 i i i 的重量为 w i w_i wi,其价值为 p i p_i pi,背包的容积为 c c c。问如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
动态规划
动态规划法是一个分阶段判定决策过程,其问题求解策略的基础是决策过程的最优原理:为达到某问题的最优目标 T T T,需要一次作出决策序列 D = D 1 , D 2 , … , D k D={D_1,D_2,…,D_k} D=D1,D2,…,Dk。如果 T T T 是最优的,则对任意 i ( 1 ≤ i ≤ k ) i(1≤i≤k) i(1≤i≤k),决策子序列 D ( i + 1 ) , D ( i + 2 ) , … , D k D_(i+1),D_(i+2),…,D_k D(i+1),D(i+2),…,Dk 也是最优的,即当前决策的最优性取决于其后续决策序列是否最优。由此追溯至目标,再由最终目标决策向上回溯,导出决策序列 D = D 1 , D 2 , … , D k D={D_1,D_2,…,D_k} D=D1,D2,…,Dk。因此动态规划方法可以保证问题求解是全局最优的。
问题分析
此问题的形式化描述是,给定
c
>
0
,
w
i
>
0
,
p
i
>
0
(
1
⩽
i
⩽
n
)
c>0, w_{i}>0, p_{i}>0 (1 \leqslant i \leqslant n)
c>0,wi>0,pi>0(1⩽i⩽n), 要求找出一个
n
n
n 元 0-1 向量
(
x
1
,
x
2
,
⋯
,
x
n
)
,
x
i
∈
{
0
,
1
}
(
1
⩽
i
⩽
n
)
,
\left(x_{1}, x_{2}, \cdots, x_{n}\right), x_{i} \in\{0,1\} \quad(1 \leqslant i \leqslant n), \quad
(x1,x2,⋯,xn),xi∈{0,1}(1⩽i⩽n), 使得
∑
i
=
1
n
w
i
x
i
⩽
c
,
\sum_{i=1}^{n} w_{i} x_{i} \leqslant \mathrm{c},
∑i=1nwixi⩽c, 而且
∑
i
=
1
n
p
i
x
i
\sum_{i=1}^{n} p_{i} x_{i}
∑i=1npixi 达到最大。因此, 0-1 背包问题是一个特殊的整数规划问题:
max
∑
i
=
1
n
p
i
x
i
{
∑
i
=
1
n
w
i
x
i
⩽
c
x
i
∈
{
0
,
1
}
1
⩽
i
⩽
n
\max \sum_{i=1}^{n} p_{i} x_{i} \quad\left\{\begin{array}{l} \sum_{i=1}^{n} w_{i} x_{i} \leqslant c \\ x_{i} \in\{0,1\} \quad 1 \leqslant i \leqslant n \end{array}\right.
maxi=1∑npixi{∑i=1nwixi⩽cxi∈{0,1}1⩽i⩽n
递归关系
m ( i , j ) m(i, j) m(i,j) 是背包容量为 j j j, 可选择物品为 i , i + 1 , ⋯ , n i, i+1, \cdots, n i,i+1,⋯,n 时 0-1背包问题的最优值。由 0-1背包问题的最优子结构性质,可以建立计算 m ( i , j ) m(i, j) m(i,j) 的递归式如下:
m ( i , j ) = { max { m ( i + 1 , j ) , m ( i + 1 , j − w i ) + p i } j ⩾ w i ( 能 装 进 去 ) m ( i + 1 , j ) 0 ⩽ j < w i ( 装 不 下 ) m ( n , j ) = { p n j ⩾ w n 0 0 ⩽ j < w n \begin{aligned} &m(i, j)=\left\{\begin{array}{ll} \max \left\{m(i+1, j), m\left(i+1, j-w_{i}\right)+p_{i}\right\} & j\geqslant w_i(能装进去) \\ m(i+1, j) & 0 \leqslant j < w_i (装不下) \end{array}\right. \\ &m(n, j)=\left\{\begin{array}{ll} p_{n} & j \geqslant w_{n} \\ 0 & 0 \leqslant j<w_{n} \end{array}\right. \end{aligned} m(i,j)={max{m(i+1,j),m(i+1,j−wi)+pi}m(i+1,j)j⩾wi(能装进去)0⩽j<wi(装不下)m(n,j)={pn0j⩾wn0⩽j<wn
C++源代码
#include <iostream>
#include <iomanip>
#include <algorithm>
#define N 100
using namespace std;
int m[N][N] = {0}; // 最优值矩阵:m[i][j] —— 背包容量为j,可选择物品 i,i+1,...,n 时的最优值
int n = 5; // 物品数量
int c = 10; // 背包容量
int p[] = {6, 3, 5, 4, 6}; // 物品价格数组
int w[] = {2, 2, 6, 5, 4}; // 物品重量数组
int x[N]; // 最优解的物品选择
// 核心函数——生成并储存dp最优值矩阵
void Knapsack_dp()
{
// 初始化最后一行(从最后往前动态规划)
for (int j = 0; j <= c; ++j)
{
m[n][j] = 0;
if (j >= w[n])
m[n][j] = p[n];
}
// 进行动态规划的主循环
for (int i = n - 1; i > 0; i--)
{
for (int j = 0; j <= c; ++j)
{
if (j < w[i]) // 装不下,背包容量在小于当前物品体积时,把上一行重复的挪下来
m[i][j] = m[i + 1][j];
if (j >= w[i]) // 装得下,选择装不装,背包容量大于当前物品体积量时,判断第i个物品是否要装
m[i][j] = max(m[i + 1][j], m[i + 1][j - w[i]] + p[i]);
}
}
// 最后得出结果的时候进行简化(因为不需要前面的数据为下一个物品做比较)
m[0][c] = m[1][c];
if (c >= w[0])
m[0][c] = max(m[1][c], m[1][c - w[0]] + p[0]);
}
// 根据最优值矩阵,查找最优解的函数
void find_best_solution()
{
int tp_c = c; // 设置一个值为背包容量的临时变量
for (int i = 0; i < n; ++i)
{
if (m[i][tp_c] == m[i + 1][tp_c]) // 说明当前容量下这个物品没有选
x[i] = 0;
else
{
x[i] = 1;
tp_c -= w[i];
}
}
// 看最后一行有没有选
x[n] = (m[n][tp_c]) ? 1 : 0;
}
// 打印最优解和最优值
void Print()
{
cout << "\n动态规划最优值矩阵:" << endl;
// 输出表头(j),背包容量
cout << setw(4) << " ";
for (int i = 0; i <= c; ++i)
{
cout << setw(4) << i;
}
cout << endl;
// 输出分割线
for (int i = -1; i <= c; ++i)
{
cout << setw(4) << "---";
}
cout << endl;
for (int i = 0; i < n; ++i)
{
cout << setw(3) << i + 1 << "|";
for (int j = 0; j <= c; ++j)
{
cout << setw(4) << m[i][j];
}
cout << endl;
}
cout << "\n最优值: " << m[0][c] << endl;
// 输出最优解
cout << "\n最优解: ";
for (int i = 0; i < n - 1; ++i)
{
cout << x[i] << "-";
}
cout << x[n - 1] << endl;
}
int main()
{
Knapsack_dp();
find_best_solution();
Print();
return 0;
}
程序输出
输出展现了0-1背包问题的最优值矩阵,算法从最优值矩阵最后一行(第5行)进入,逐步向上走,每一行的更新都依赖于之前一行(下面的一行)的数据。最优值矩阵的第一行(最后计算的一行)不需要被依赖,因此可以直接简化计算最后一位,得出结果。
图中红色框框标识的是在通过最优值矩阵查找最优解的关键点。
输出的最后是0-1向量,表示了该位置的物品是否选择(1为选择,0为不选择),如图用例选择了①②⑤号物品,此时达到最优值15。