0-1背包问题(Java详解)
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
动态规划的应用场景
适用动态规划的问题必须满足最优化原理、无后效性和重叠性。
a.最优化原理(最优子结构性质)
最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
b.无后效性
将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
c.子问题的重叠性
动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的算法,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
1.问题描述
有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?
为方便讲解和理解,下面讲述的例子均先用具体的数字代入,即:(数量)N=4,(体积)V=8
i(物品编号) | 1 | 2 | 3 | 4 |
weight(体积) | 2 | 3 | 4 | 5 |
value(价值) | 3 | 4 | 5 | 6 |
2.总体思路
根据动态规划解题步骤(问题抽象化、建立模型、寻找约束条件、判断是否满足最优性原理、找大问题与小问题的递推关系式、填表、寻找解组成)找出01背包问题的最优解以及解组成,然后编写代码实现。
3.动态规划的原理
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。
最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。
4.背包问题的解决过程
在解决问题之前,为描述方便,首先定义一些变量:value(i)表示第 i 个物品的价值,weight(i)表示第 i 个物品的体积,定义dp(i, j):当前背包容量 j,前 i 个物品最佳组合 对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。
1、建立模型,即求max(value(1)X1+value(2)X2+…+value(n)Xn);
2、寻找约束条件,weight(1)X1+weight(2)X2+…+weight(n)Xn < V;
3、寻找递推关系式,面对当前商品有两种可能性:
- 包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即dp(i, j) = dp(i-1, j);
- 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即dp(i, j) = max{dp(i-1, j),dp(i-1, j - weight(i) ) + value(i)}。
其中dp(i-1, j)表示不装,dp( i-1, j-weight(i) ) + value(i) 表示装了第i个商品,背包容量减少weight(i),但价值增加了value(i);
由此可以得出递推关系式:
(1)j < weight(i), dp(i, j) = dp(i-1, j)
(2)j >= weight(i), dp(i, j) = max{dp(i-1, j),dp(i-1, j-weight(i)) + value(i)}
其中dp(i-1,j)表示装的下,却不装
(1)式表明:如果第i个物品的重量大于背包的容量,则装人前i个物品得到的最大价值和装入前i-1个物品得到的最大价是相同的,即物品i不能装入背包;
第(2)个式子表明:如果第i个物品的重量小于背包的容量,则会有一下两种情况:(a)如果把第i个物品装入背包,则背包物品的价值等于第i-1个物品装入容量位j-weight(i) 的背包中的价值加上第i个物品的价值value(i); (b)如果第i个物品没有装入背包,则背包中物品价值就等于把前i-1个物品装入容量为j的背包中所取得的价值。显然,取二者中价值最大的,作为把前i个物品装入容量为j的背包中的最优解。
这里需要解释一下,为什么背包容量足够的情况下,还需要 dp(i, j) = max{dp(i-1, j),dp(i-1, j-weight(i)) + value(i)}?
dp(i-1,j-weight(i))+value(i) 表示装了第i个物品后背包中的最大价值,所以当前背包容量 j 中,必定有weight(i)个容量给了第i个背包。
因此只剩余j-weight(i)个容量用来装,除了第i件物品的其他所有物品。
dp(i-1,j-weight(i))是前i-1个物品装入容量为j-weight(i)的背包中最大价值。
注意,这里有一个问题。前i-1个物品装入容量为j-weight(i)的背包中最大价值+物品i的价值。可能不如将,前i-1个物品装入容量为j的背包中得到的价值大。也就是说,可能出现 dp(i-1,j) > (dp(i-1,j-weight(i))+value(i))
比如说,将第i个物品放入背包,可能会导致前面更有价值的物品放不进背包。因此,还不如不把第i个物品放进去,把空间让出来,从而能让前i-1个物品中更有价值的物品能够放入背包。从而让V(i,j)取得最大的值。
所以我们需要 max{dp(i-1,j),dp(i-1,j-weight(i))+value(i)},来作为把前i个物品装入容量为j的背包中的最优解。
4、填表,首先初始化边界条件,dp(0,j) = dp(i,0) = 0;
然后一行一行的填表:
- 如,i=1,j=1,weight(1)=2,value(1)=3,有j<weight(1),故dp(1,1)=dp(1-1,1)=0;
- 又如i=1,j=2,weight(1)=2,value(1)=3,有j=weight(1),故dp(1,2)=max{ dp(1-1,2),dp(1-1,2-weight(1))+value(1) }=max{0,0+3}=3;
- 如此下去,填到最后一个,i=4,j=8,weight(4)=5,value(4)=6,有j>weight(4),故dp(4,8)=max{ dp(4-1,8),dp(4-1,8-weight(4))+value(4) }=max{9,4+6}=10……
所以填完表如下图:
5、表格填完,最优解即是dp(N,V)=dp(4,8)=10。(也就是说,在有4个可选物品,背包容量为8的情况下,能装入的最大价值为10)。
5.背包问题最优解回溯(求解 这个最优解由哪些商品组成?)
通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:
- dp(i,j)=dp(i-1,j)时,说明没有选择第i 个商品,则回到dp(i-1,j);
- dp(i,j)=dp(i-1,j-weight(i))+value(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到dp(i-1,j-weight(i));
- 一直遍历到i=0结束为止,所有解的组成都会找到。
就拿上面的例子来说吧:
- 最优解为dp(4,8)=10,而dp(4,8)!=dp(3,8)却有dp(4,8)=dp(3,8-weight(4))+value(4)=dp(3,3)+6=4+6=10,所以第4件商品被选中,并且回到dp(3,8-weight(4))=dp(3,3);
- 有dp(3,3)=dp(2,3)=4,所以第3件商品没被选择,回到dp(2,3);
- 而dp(2,3)!=dp(1,3)却有dp(2,3)=dp(1,3-weight(2))+value(2)=dp(1,0)+4=0+4=4,所以第2件商品被选中,并且回到dp(1,3-weight(2))=dp(1,0);
- 有dp(1,0)=dp(0,0)=0,所以第1件商品没被选择。
动态规划法求解0/1背包问题:
1)基本思想:
令表示在前个物品中能够装入容量为的背包中的物品的最大值,则可以得到如下动态函数:
代码实现
为了和之前的动态规划图可以进行对比,尽管只有4个商品,但是我们创建的数组长度为5。。
package 动态规划.背包问题;/*
*author:yangyu
*creation time:2023/6/1 22:17
*/
import java.util.Arrays;
public class ZeroOnePack {
public static void main(String[] args) {
int V = 8;
int N = 4;
int[] weight = new int[]{0, 2, 3, 4, 5};
int[] value = new int[]{0, 3, 4, 5, 6};
System.out.println("背包容量:" + V);
System.out.println("物品数量:" + N);
System.out.println("物品容量:" + Arrays.toString(weight));
System.out.println("物品价值:" + Arrays.toString(value));
getArticles(V, N, weight, value);
}
/**
* 求解01背包问题
*
* @param V 背包容量
* @param N 物品种类
* @param weight 物品重量
* @param value 物品价值
* @return
*/
private static int[][] zeroOnePack(int V, int N, int[] weight, int[] value) {
int[][] dp = new int[N + 1][V + 1];
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= V; j++) {
//由于weight和value数组下标都是从 1 开始,故注意第i个物品的重量为weight[i],价值为value[i]
//如果第i件物品的重量大于背包容量j,则第i件物品不装入背包,存放前i-1个物品的价值
if (weight[i] > j)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
return dp;
}
/**
* 已知价值最大,求背包内装入了哪些物品?
*
* @param V 背包容量
* @param N 物品种类
* @param weight 物品重量
* @param value 物品价值
* @return
*/
private static void getArticles(int V, int N, int[] weight, int[] value) {
int[][] dp = zeroOnePack(V, N, weight, value);
System.out.println("动态规划表如下:");
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= V; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println();
}
System.out.println("背包内最大的物品价值总和为:" + dp[N][V]);
boolean[] flags = new boolean[N + 1];
for (int i = N; i > 0; i--) {
//如果dp[i][j] == dp[i-1][j],则说明第i个物品没有放入dp[i][j]中,否则反之
if (dp[i][V] > dp[i - 1][V]) {
flags[i] = true;
//放入物品后背包容量减少
V -= weight[i];
}
}
System.out.print("包内物品的编号为:");
for (int i = 1; i <= N; i++) {
if (flags[i]) {
System.out.print(i + " ");
}
}
}
}
测试结果:
背包容量:8
物品数量:4
物品容量:[0, 2, 3, 4, 5]
物品价值:[0, 3, 4, 5, 6]
动态规划表如下:
0 3 3 3 3 3 3 3
0 3 4 4 7 7 7 7
0 3 4 5 7 8 9 9
0 3 4 5 7 8 9 10
背包内最大的物品价值总和为:10
包内物品的编号为:2 4
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)