01背包问题
01背包问题
背包问题
背包问题主要包含以下3种基本的问题:
- 01背包
- 完全背包
- 多重背包
其中对于每一种xx背包问题还存在一个特殊的情形,即要求背包恰好被装满,这种特殊问题的求解主要是在动态规划的状态数组的初始化做一下特殊的处理。
除此之外,有时候我们不仅仅要求背包能装下的最大物品的价值,我们还希望得到具体的装包方案,这里就会涉及到状态数组的回溯(Track),下面会举例说明。
问题描述
01背包问题是背包问题中最简单最基础的一类问题,问题描述如下:
给定件物品,对于第件物品,其价值为,重量为,与此同时还存在一个体积为的背包,每件物品只有一件,因此每件物品可以选择是否放进背包,求背包能装下的物品的最大价值。
对于该问题,对应的数学模型是一个简单的01规划问题:
对应的模型为:
解法
01背包问题常常采用动态规划的方法去求解,状态转移方程为:
表示前种物品装进容量为的背包里面获取的最大价值,因此对于第件物品来放进大小为的背包来讲:
- 如果,那么该物品放不进去,则此时的收益和前件物品放进大小为的背包的最大收益一样,即;
- 若果, 则该物品可以放进去,但是此时是有两种选择的,即放进去或者不放进去,因此需要评估两种选择的收益大小:
- 将第件物品不放进去:收益为
- 将第件物品放进去,那么此时前一个状态只可能是:前件物品放进大小为的背包中。因此将第件物品放入背包的收益是
- 最后两个选择中最大的便是当前的收益
代码如下:
public static int knapsackProblemZeroOne(int[] value, int[] weight, int bagV) {
int[][] dp = new int[value.length][bagV + 1];
// dp[i][j] 表示前i个物品,装填大小为j的背包所能达到的最大价值;
// 显然因为value[0] = 0,dp[0][0~bagV] = 0 ;
for (int i = 1; i < value.length; i++) {
for (int j = 1; j <= bagV; j++) {
if (weight[i] > j) {// 第i个物品放不进去
dp[i][j] = dp[i - 1][j];
} else {// 第i个物品能放进去
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
return dp[value.length - 1][bagV];
}
上述代码里面的两层for循环,由于是从1开始的,因此对于对于参数value
,里面的有效数据应该也是从1开始计数的,因此在value
数组和weight
数组的第一个元素都应该置为0。
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] value = new int[] { 0, 1, 4, 3,6};// 物品的价值
int[] weight = new int[] { 0, 5, 2, 4, 3};// 物品的重量
int bagV = 15;// 背包的大小
System.out.println(knapsackProblemZeroOne(value, weight, bagV));
}
实现细节:
- value和weight数组的第一个元素应该都为0,以便后续处理可以从1开始计数;
- dp状态数组的列下标也是从1开始计数的,因此申请空间的时候应该是dp[n][bagV+1];
dp数组的求解正如如下表格一样,从左上到右下依次求解:
空间优化
上述算法的时间复杂度为,其中为物品的数量,为背包的体积。
但是通过上述的状态转移方程我们发现,其实的值仅仅和, 有关,也就是说第件物品只和前一件物品有关(dp二维数组的第i行可以通过第i-1行推出来),因此我们可以将二维数组改写为一维数组,俗称滚动数组。
public static int knapsackProblemZeroOneOptimization(int[] value, int[] weight, int bagV) {
int[] dp = new int[bagV + 1]; // dp[i][j] 表示前i个物品,装填大小为j的背包所能达到的最大价值
// 显然,dp[0][0~bagV] = 0, dp[0~v.length-1][0] = 0;
for (int i = 1; i < value.length; i++) {
for (int j = bagV; j >= weight[i]; j--) {//从后往前更新,因为后一项状态dp[j]需要利用上一轮的j前面的状态;
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp[bagV];
}
关键要理解这里的写法:
for (int j = bagV; j >= weight[i]; j--)
经过此番优化后,时间复杂度不变,空间复杂度降为。
状态回溯
以上的算法只是给出能够得到的最大收益值,但是有时候我们希望得到具体的装包方案,即我们需要知道哪些物品需要装包,哪些不需要。
根据状态数组从后往前倒退,即可得到一条路径,将该路径上的信息保存便可以得到方案。
对于dp[i][j],该值来源于两个地方:
- dp[i-1][j]
- dp[i-1][j-wi]
因此可以写出下面的代码求得装包方案:
public static int[] tracback(int[][] dp, int[] weight, int bagV) {
int[] res = new int[weight.length];
int j = bagV;
for (int i = weight.length - 1; i >= 1; i--) {//从后往前推
if (dp[i][j] == dp[i - 1][j]) {//dp[i][j]来源于dp[i-1][j]说明第i件物品装不进去
res[i] = 0;
} else {
res[i] = 1;
j -= weight[i];
}
}
res[0] = dp[0][j] > 0 ? 1 : 0;
return res;
}
状态回溯的求解正如如下二维表格一样,从右下往左上倒推:
references
上一篇: AC自动机
推荐阅读
-
困扰JSP的一些问题与解决方法
-
python logging 日志轮转文件不删除问题的解决方法
-
ToolBar中menu无法同时显示图标和文字问题的解决方法
-
Java算法之最长公共子序列问题(LCS)实例分析
-
用Python解决计数原理问题的方法
-
Mysql启动中 InnoDB: Error: log file ./ib_logfile0 is of different size 0 5242880 bytes 的问题
-
解决表单post,get到springMVC后台乱码的问题
-
mysql建立自定义函数的问题
-
解决Android应用冷启动时出现的白屏问题的方法
-
完美解决node.js中使用https请求报CERT_UNTRUSTED的问题