基于动态规划方法求解0-1背包问题
问题描述
现有n个物品,1个背包。对物品i,其价值为 v i v_i vi ,重量为 W i W_i Wi,背包的容量为 W W W,如何选取物品使得背包里转入物品的总价值最大?
在约束条件为:选取物品的重量小于等于背包重量的情况下,尽可能让背包中物品的总价值最大。
算法设计
根据问题描述,约束条件为,目标函数为:
在满足约束条件的情况下,找到是目标函数最大的解。
使用两个等长的一维数组存储物品的重量 w e i g h t s [ n ] weights[n] weights[n]和价值 v a l u e s [ n ] values[n] values[n],这里第 i i i个物品的重量就是 w e i g h t s [ i ] weights[i] weights[i],价值就是 v a l u e s [ i ] values[i] values[i]。
使用动态规划的方式求解该问题,构建二维数组 d p [ n + 1 ] [ W + 1 ] dp[n+1][W+1] dp[n+1][W+1]来记录中间过程的最优解,这里 d p [ i ] [ j ] dp[i][j] dp[i][j]表示当背包容量为 j j j时,装前 i i i个物品的最优解。
描述算法
依次考虑前 i − 1 i-1 i−1个物品装入背包的最优解,那么装入前 i i i个物品的最优解就是在前 i − 1 i-1 i−1个物品的最优解上进行构建
如果只考虑第i件物品放还是不放,那么就可以转化为一个只涉及到前i-1个物品的问题。如果不放第i个物品,那么问题就转化为“前i-1件物品放入容量为j的背包中的最优价值组合”,对应的值为 d p [ i − 1 , j ] dp[i-1,j] dp[i−1,j]。如果放第i个物品,那么问题就转化成了“前i-1件物品放入容量为 j − w i j-w_i j−wi的背包中的最优价值组合”,此时对应的值为 d p [ i − 1 , j − W i ) ] + v i dp[i-1,j-W_i)]+v_i dp[i−1,j−Wi)]+vi。
-
d p [ i ] [ 0 ] = d p [ 0 ] [ j ] = 0 dp[i][0]=dp[0][j]=0 dp[i][0]=dp[0][j]=0
-
如果第 i i i个 物品重量大于背包总重量 j j j,那么 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j]
-
如果第 i i i个 物品重量小于等于背包总重量 j j j,那么 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t s [ i ] ] + v a l u e s [ i ] dp[i][j]=max(dp[i-1][j],dp[i-1][j-weights[i]]+values[i] dp[i][j]=max(dp[i−1][j],dp[i−1][j−weights[i]]+values[i]
01PACKAGE(values[n],weights[n],W) // W为背包容量 dp[n+1][W+1] for i=0 to n: do dp[i][0] = 0 for j=0 to W: do dp[0][j] = 0 for i=1 to n: for j=1 to W: do if j < weights[i]: dp[i][j]=dp[i-1][j] else dp[i][j]= max(dp[i-1][j],dp[i-1][j-weights[i]]+values[i]) return C
算法的正确性证明
假设
(
x
1
,
x
2
,
…
,
x
n
)
(x_1,x_2,…,x_n)
(x1,x2,…,xn)是01背包问题的最优解,则有
(
x
2
,
x
3
,
…
,
x
n
)
(x_2,x_3,…,x_n)
(x2,x3,…,xn)是其子问题的最优解,假设
(
y
2
,
y
3
,
…
,
y
n
)
(y_2,y_3,…,y_n)
(y2,y3,…,yn)是上述问题的子问题最优解,则有
(
v
2
y
2
+
v
3
y
3
+
…
+
v
n
y
n
)
+
v
1
x
1
>
(
v
2
x
2
+
v
3
x
3
+
…
+
v
n
x
n
)
+
v
1
x
1
(v_2y_2+v_3y_3+…+v_ny_n)+v_1x_1 > (v_2x_2+v_3x_3+…+v_nx_n)+v_1x_1
(v2y2+v3y3+…+vnyn)+v1x1>(v2x2+v3x3+…+vnxn)+v1x1。说明(X_1,Y_2,Y_3,…,Y_n)才是该01背包问题的最优解,这与最开始的假设(X_1,X_2,…,X_n)是01背包问题的最优解相矛盾,故01背包问题满足最优性原理
。
至于无后效性
,其实比较好理解。对于任意一个阶段,只要背包剩余容量和可选物品是一样的,那么我们能做出的现阶段的最优选择必定是一样的,是不受之前选择了什么物品所影响的。即满足无后效性
。
算法复发性分析
时间复杂度:
空间复杂度:
算法实现与测试
public class Main {
public static void main(String[] args) {
int weights[] = {2, 6, 3, 4, 2, 8, 2, 4, 7, 5, 1};
int values[] = {10,23,5,34,23,17,22,32,12,15,32};
int W=15;
int dp[][] = packet01(W,weights,values);
int res[] = new int[W];
int j = W;
for (int i = weights.length; i > 0; i--) {
if(dp[i][j] == dp[i-1][j])
res[i-1] = 0;
else{
res[i-1] = 1;
j -= weights[i-1];
}
}
System.out.println("res: "+dp[weights.length][W] );
System.out.println("packets: ");
for (int i = 0; i < W; i++) {
System.out.print(res[i] +" ");
}
}
public static int[][] packet01(int W, int weights[], int values[]) {
int n = weights.length;
int [][]dp = new int[n+1][W+1];
for (int i = 0; i <= n; i++) {
dp[i][0] = 0;
}
for (int j = 0; j <= W ; j++) {
dp[0][j] = 0;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= W; j++) {
int currentItem = i-1;
if(j < weights[currentItem]){
dp[i][j] = dp[i-1][j];
} else {
dp[i][j] = Math.max(dp[i-1][j],
dp[i-1][j-weights[currentItem]] + values[currentItem]);
}
}
}
return dp;
}
}
测试:
res: 153
packets:
1 0 0 1 1 0 1 1 0 0 1 0 0 0 0
心得体会
动态规划算法通常是用来解决某种最优性质的问题。基本思想是将带求解问题划分为若干个子问题,先求解子问题,然后从子问题的解得到原问题的解。动态规划与分治法的区别在于,动态规划的子问题可能是互相重叠的重复计算的,分治法则是相互独立的。可以用一个表来记录子问题是否已经求解,这样可以避免重复求解。
动态规划需要满足:
-
最优化原理,一个最优化策略的子策略一定是最优的,就是满足最优子结构的性质。
-
无后效性,一个阶段以前各阶段的状态无法直接硬性它未来的决策,只能通过当前的这个状态。
-
重叠性,就是记录已经解决过的问题,需要存储已经解决过的问题,空间复杂度比较大,是一种以空间换时间的算法。
动态规划的难点在于,如何根据问题的最优子结构的性质,构造动态规划方法中的递归公式或动态规划方程。就比如本问题中,如何设计这个方程才是难点所在。