0-1背包问题的Dynamic Program思路
0-1背包问题:对于一组不同重量、不可分割的物品,物品有n个,选择一些物品装入背包,在满足背包最大承重w的前提下求出背包的最大重量。
这个问题可以用回溯算法实现,可以参考《回溯算法》这一篇,也可以使用动态规划的思想去解决。
所谓动态规划,简单说就是把一个问题分为多个阶段,每个阶段对应一个决策Y or N,使用一个状态数组记录记录每一个阶段的状态,根据当前阶段的状态数组,推导出下一个阶段的状态数组,动态地向前推进。
对于背包问题就是,将问题分为n个阶段,n表示物品个数,每个阶段都会决策这个物品是否要放入背包中:放入或者不放入。决策执行之后,背包中的物品重量就会出现多种状态,对应到递归树中就是就是有很多不同节点。我们把递归树每一层中重复的节点合并,只记录不同的状态,然后基于上一层的状态数组,推导出下一阶段的状态数组。每一层中的状态个数最多为w个即背包的承重。使用二维数组states[n][w+1]表示状态数组,记录每个物品决策执行后可以达到的不同状态。
问题分析
这里例子中物品重量为数组:[2,2,4,6,3].
- 第0个物品的重量是2,要么装入,要么不装入,对应背包重量也有两种状态0或者2,那么就用states[0][0]=true和states[0][2]=true表示两种状态。
- 第1个物品的重量是2,要么装入,要么不装入,对应背包重量有三种状态0或者2或者4,那么就用states[1][0]=true和states[1][2]=true和states[1][4]=true表示三种状态。
- 依次类推,把所有物品决策完之后,状态数组states也计算完成,我们只需要在最后一层找一个为true且最接近w的值,就是我们最终的满足w承重下的最大重量。如图:
代码:
private int[] weight = {2,2,4,6,3};
/**
* 物品个数
*/
private int n = 5;
/**
* 背包承受的最大重量
*/
private int w = 9;
/**
* weight: 物品重量,n: 物品个数,w: 背包可承载重量
*/
public int f(int[] weight, int n, int w) {
// 默认值 false
boolean[][] states = new boolean[n][w+1];
// 第一行的数据要特殊处理,可以利用哨兵优化
states[0][0] = true;
if (weight[0] <= w) {
states[0][weight[0]] = true;
}
// 动态规划状态转移
for (int i = 1; i < n; ++i) {
// 不把第 i 个物品放入背包
for (int j = 0; j <= w; ++j) {
if (states[i-1][j] == true){
states[i][j] = states[i-1][j];
}
}
// 把第 i 个物品放入背包
for (int j = 0; j <= w-weight[i]; ++j) {
if (states[i-1][j]==true){
states[i][ j + weight[i] ] = true;
}
}
}
// 输出结果
for (int i = w; i >= 0; --i) {
if (states[n-1][i] == true) {
return i;
}
}
return 0;
}
时间和空间复杂度分析
时间复杂度从代码的两层循环中可以看出O(n*w),n表示物品个数,w表示总承重。
空间复杂度因为要申请一个n乘以w+1的二维数组,所以内存消耗比较大,所以是空间换时间的思路,不过可以优化。
就是使用一个一维数组来代替二维数组。
优化代码如下:
/**
* 动态规划空间复杂度优化(相对于上边的f函数)
* @param weight: 物品重量 [2,2,4,6,3],n: 物品个数,w: 背包可承载重量 9
* @return
*/
public int f2(int[] weight, int n, int w) {
// 默认值 false
boolean[] states = new boolean[w + 1];
// 第一行的数据要特殊处理,可以利用哨兵优化
states[0] = true;
if (weight[0] <= w) {
states[weight[0]] = true;
}
// 动态规划
for (int i = 1; i < n; ++i) {
// 把第 i 个物品放入背包
for (int j = w - weight[i]; j >= 0; --j) {
/**1、j初始值:w-weight[i],往后j--;
2、如果j位置的states[j]为true,再放入weight[i]这个物品,j+weight[i]这个位置就被设置为true。
否则j位置为false,检查j--这个位置(往前)的状态是否为true。
*/
if (states[j] == true) {
states[ j + weight[i] ] = true;
}
}
}
// 输出结果
for (int i = w; i >= 0; --i) {
if (states[i] == true) {
return i;
}
}
return 0;
}
0-1背包问题的升级
在考虑背包总承重下物品最大重量的前提下,再加入一个物品的价值,计算最终的背包中可以容纳的最大总价值是多少?
代码:
/**
物品的总价值
*/
private int[] value = {3,4,8,9,6};
/**
* 在考虑物品价值的情况下,计算在承重限制条件下的背包中最大总价值
* @param weight 物品总量数组
* @param value 物品价值数组
* @param n 物品个数
* @param w 背包承重
* @return
*/
public int superF(int[] weight, int[] value, int n, int w) {
//states记录当前状态下的物品最大值
int[][] states = new int[n][w+1];
// 初始化 states
for (int i = 0; i < n; ++i) {
for (int j = 0; j < w+1; ++j) {
states[i][j] = -1;
}
}
states[0][0] = 0;
if (weight[0] <= w) {
states[0][ weight[0] ] = value[0];
}
// 动态规划,状态转移
for (int i = 1; i < n; ++i) {
// 不选择第 i 个物品
for (int j = 0; j <= w; ++j) {
if (states[i-1][j] >= 0) {
states[i][j] = states[i-1][j];
}
}
// 选择第 i 个物品
for (int j = 0; j <= w - weight[i]; ++j) {
//迭代上一行j这个重量的位置的价值大于等于0时的逻辑
if (states[i-1][j] >= 0) {
//当前第i个物品的第j重量所在列的价值,求和第i个物品价值之后的总价值(如果需要,则最终也只应该放在j+weight[i]这个重量位置处)
int v = states[i-1][j] + value[i];
//第i个物品选择后,除了价值增加,重量也将要增加,增加后重量就是j+weight[i],那么在j+weight[i]这个位置的价值如果小于v,则更新这个位置的价值
/**
* 之前是重量j的位置设置为true表示放置了重量(然后就更新j+weight[i]为true,表示当前放置后的重量为j+weight[i])
* 现在判断v是否大于j+weight[i]大小重量的位置的价值,如果大于那个位置的价值,才会将v放置到那个位置,否则就是小于等于那个位置的价值,没必要更新,继续迭代,j++,
*/
if (v > states[i][ j + weight[i] ]) {
states[i][ j + weight[i] ] = v;
}
}
}
}
// 找出最大值
int maxvalue = -1;
for (int j = 0; j <= w; ++j) {
if (states[n-1][j] > maxvalue) {
maxvalue = states[n-1][j];
}
}
return maxvalue;
}