文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。
1 0-1背包问题
背包能够承受的总重量一定w,每个物品的总量不同int[] weight表示。怎么放才能让背包中物品的总重量最大。
每次决定一种物品,要不要放入到背包中。当物品放完了或者总重量等于w,就停止放入,选择最大的总量保存下来。
2 用回溯法实现
public class Package {
private int[] weight = new int[]{2,2,4,6,3};
private int n = 5;//物品个数
private int w = 9;//背包承受的最大重量
private int maxW = Integer.MIN_VALUE;//结果
/**
* 处理第i个物品的情况,当前重量是cw
* 这是回溯法,复杂度是指数级的。有些状态会计算多次。
* @param i
* @param cw
*/
public void f(int i,int cw){
if(cw==w || i==n){
maxW =Math.max(cw,maxW);
return;
}
f(i+1,cw);//第i个物品,不装入背包
if(cw+weight[i]<=w){
f(i+1,cw+weight[i]);//第i个物品,装入背包
}
}
public int maxWeight(){
f(0,0)
return maxW;
}
}
我们根据上面这个特殊的例子,把回溯求解问题的递归树画出来。
递归树中的每个节点表示一个状态,用(i,cw)表示。i 表示要将要处理第i个物品,cw表示当前总重量。例如(2,2)表示我们将要处理第2个物品,在处理之前已经放入的物品总重量是2。
从递归树中能看到某些状态被重复计算了,例如f(2, 2) 和 f(3,4)被计算了两次。为了解决这个问题,可以有两种方法解决。
3 第一种:备忘录
我们可以使用备忘录,遇到状态已经计算过的就不再计算了。改进代码如下。
private boolean[][] mem = new boolean[n][w+1];
/**
* 记录状态,已经计算过的状态就不再计算了
* @param i
* @param cw
*/
public void fV2(int i,int cw){
if(cw==w || i==n){
maxW =Math.max(cw,maxW);
return;
}
if(mem[i][cw]) return;
mem[i][cw] = true;
f(i+1,cw);//第i个物品,不装入背包
if(cw+weight[i]<=w){
f(i+1,cw+weight[i]);//第i个物品,装入背包
}
}
4 第二种:动态规划
我们把整个过程看做n个阶段,每个阶段只决策一种物品是否放入。每个物品决策(放或者不放)完成之后,背包中物品的重量会有多种情况。也就是说会有多种状态,对应递归树中不同的节点。
我们把每一层重复的节点合并,只记录不同的状态。基于上一层的状态集合,推导下一层集合的状态。我们合并每一层的状态,保证每一层节点个数不会超过w个。这样就避免了每一层状态节点个数指数级增长。
我们用states[n][w+1]来记录每一层可以达到的不同状态。例如上面例子中分析有(2,2)这个节点,那么states[2][2]=true。
第0个物品的重量是2,要么装入背包,要么不装入背包,决策之后会对应背包中的两种状态,背包中的总总量是0或者2.我们用state[0][0]=true,state[0][2]=true来表示这两种状态。
第1个物品的重量是2,要么装入背包,要么不装入背包,决策之后对应的背包状态:
0+0=0
0+2=2
2+2=4
这是基于上一步背包的状态计算得到的
我们用state[1][0]=true state[1][2]=true state[1][4]=true 来表示。
以此类推,一直到第n-1个物品。找到state[n-1] 的 数组中找到最大的state[n-1][j]=true,返回j。
4.1 状态表
这个过程用状态表来表示,就是下图。
代码如下。代码时间复杂度O(n*w)。
public int knapsnack(int[] weight,int n,int w){
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++){
for(int j=0;j<w;j++){
if(states[i-1][j]==true){
states[i][j] = true;
}
}
for(int j=0;j<=w-weight[i];j++){
if(states[i-1][j]==true){
states[i][j+weight[i]] = true;
}
}
}
for(int j=w;j>=0;j--){
if(states[n-1][j]) return j;
}
return 0;
}
上面的代码实现用到二维数组。经过观察,我们发现,每次for循环里面,在计算states[i]的时候,只与states[i-1]有关系。我们应该只用一维数组就能实现。
public int knapsnackV2(int[] weight,int n,int w){
boolean[] states = new boolean[w+1];
states[0] = true;
if(weight[0]<w){
states[weight[0]] = true;
}
for(int i=1;i<n;i++){
//使用一维数组需要从后向前计算,否则会有多余的计算
for(int j=w-weight[i];j>=0;j--){
if(states[j]==true){
states[j+weight[i]] = true;
}
}
}
for(int j=w;j>=0;j--){
if(states[j]) return j;
}
return 0;
}
4.2 状态方程
这道题目用状态方程来表示不太好表示。
2021-10-25:再次看这个状态方程是可以表示的。
state[i][j]=true表示当第i个物品决策完之后,背包可能的重量是j。
state[i][j]=false表示当第i个物品决策完之后,背包不可能是j。
state[i][j]=true, if state[i-1][j]=true
state[i][j+weights[i]] = true, if state[i-1][j]=true and j+weights[i]<=w(不超重)