文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。
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(不超重)