题目
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
0-1背包问题指的是每个物品只能使用一次
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
递归方法
首先我们用递归的方式来尝试解决这个问题
我们用F(n,C))表示将前n个物品放进容量为C的背包里,得到的最大的价值。
我们用自顶向下的角度来看,假如我们已经进行到了最后一步(即求解将n个物品放到背包里获得的最大价值),此时我们便有两种选择
- 不放第n个物品,此时总价值为
F(n−1,C)
- 放置第n个物品,此时总价值为
Vn+F(n−1,C−Wn)
两种选择中总价值最大的方案就是我们的最终方案,递推式(有时也称之为状态转移方程)如下
F(i,C)=max(F(i−1,C),v(i)+F(i−1,C−w(i)))
java代码实现如下:
public class KnapSack01 {
/**
* 解决背包问题的递归函数
*
* @param w 物品的重量数组
* @param v 物品的价值数组
* @param index 当前待选择的物品索引
* @param capacity 当前背包有效容量
* @return 最大价值
*/
private static int solveKS(int[] w, int[] v, int index, int capacity) {
//基准条件:如果索引无效或者容量不足,直接返回当前价值0
if (index < 0 || capacity <= 0)
return 0;
//不放第index个物品所得价值
int res = solveKS(w, v, index - 1, capacity);
//放第index个物品所得价值(前提是:第index个物品可以放得下)
if (w[index] <= capacity) {
res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index]));
}
return res;
}
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
return solveKS(w, v, size - 1, C);
}
public static void main(String[] args){
int[] w = {2,1,3,2};
int[] v = {12,10,20,15};
System.out.println(knapSack(w,v,5));
}
}
记忆化搜索
我们用递归方法可以很简单的实现以上代码,但是有个严重的问题就是,直接采用自顶向下的递归算法会导致要不止一次的解决公共子问题,因此效率是相当低下的。
我们可以将已经求得的子问题的结果保存下来,这样对子问题只会求解一次,这便是记忆化搜索
public class KnapSack02 {
private static int[][] memo;
/**
* 解决背包问题的递归函数
*
* @param w 物品的重量数组
* @param v 物品的价值数组
* @param index 当前待选择的物品索引
* @param capacity 当前背包有效容量
* @return 最大价值
*/
private static int solveKS(int[] w, int[] v, int index, int capacity) {
//基准条件:如果索引无效或者容量不足,直接返回当前价值0
System.out.println();
for (int i = 0; i < memo.length; i++) {
for (int j = 0; j < memo[i].length; j++) {
System.out.print("meo["+i+"]["+j+"] = "+ memo[i][j]+" ");
}
System.out.println();
}
if (index < 0 || capacity <= 0)
return 0;
//如果此子问题已经求解过,则直接返回上次求解的结果
if (memo[index][capacity] != 0) {
return memo[index][capacity];
}
//不放第index个物品所得价值
int res = solveKS(w, v, index - 1, capacity);
//放第index个物品所得价值(前提是:第index个物品可以放得下)
if (w[index] <= capacity) {
res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index]));
}
//添加子问题的解,便于下次直接使用
memo[index][capacity] = res;
return res;
}
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
System.out.println("size = "+size+" C+1 = "+(C+1));
memo = new int[size][C + 1];
return solveKS(w, v, size - 1, C);
}
public static void main(String[] args) {
int[] w = {2, 1, 3, 2};
int[] v = {12, 10, 20, 15};
System.out.println(knapSack(w, v, 5));
}
}
动态规划算法
public class KnapSack01 {
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
if (size == 0) {
return 0;
}
int[][] dp = new int[size][C + 1];
//初始化第一行
//仅考虑容量为C的背包放第0个物品的情况
for (int i = 0; i <= C; i++) {
dp[0][i] = w[0] <= i ? v[0] : 0;
}
//填充其他行和列
for (int i = 1; i < size; i++) {
for (int j = 0; j <= C; j++) {
dp[i][j] = dp[i - 1][j];
if (w[i] <= j) {
dp[i][j] = Math.max(dp[i][j], v[i] + dp[i - 1][j - w[i]]);
}
}
}
return dp[size - 1][C];
}
public static void main(String[] args) {
int[] w = {2, 1, 3, 2};
int[] v = {12, 10, 20, 15};
System.out.println(knapSack(w, v, 5));
}
可能动态规划算法理解有点困难,这里我们可以通过画一个表格来理解,行表示 容量,从0开始,列表示放入第0 1 2 …个物品来查看一下。
空间复杂度的极致优化
上面的动态规划算法使用了O(n*C)的空间复杂度(因为我们使用了二维数组来记录子问题的解),其实我们完全可以只使用一维数组来存放结果,但同时我们需要注意的是,为了防止计算结果被覆盖,我们必须从后向前分别进行计算。
我们仍然假设背包空间为5,根据
F(i,C)=max(F(i−1,C),v(i)+F(i−1,C−w(i)))
我们可以知道,当我们利用一维数组进行记忆化的时候,我们只需要使用到当前位置的值和该位置之前的值,举个例子
假设我们要计算F(i,4),我们需要用到的值为F(i−1,4)和F(i-1,4−w(i)) ,因此为了防止结果被覆盖,我们需要从后向前依次计算结果
最终的动态规划代码如下
public class KnapSack01 {
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
if (size == 0) {
return 0;
}
int[] dp = new int[C + 1];
//初始化第一行
//仅考虑容量为C的背包放第0个物品的情况
for (int i = 0; i <= C; i++) {
dp[i] = w[0] <= i ? v[0] : 0;
}
for (int i = 1; i < size; i++) {
for (int j = C; j >= w[i]; j--) {
dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
}
}
return dp[C];
}
public static void main(String[] args) {
int[] w = {2, 1, 3, 2};
int[] v = {12, 10, 20, 15};
System.out.println(knapSack(w, v, 5));
}
}