一、记忆化搜索与动态规划
#01 背包问题
n = 4
(w, v) = { (2, 3), (1, 2), (3, 4) , (2, 2) }
W = 5
7(选择第0 、1 、3 号物品)
【解析】
第一步:先写限制条件,本题有两个(遍历到底,重量不能超过 W )。
遍历到底,回溯;
超重,跳过这个物品;
第二步:分析主结构:
每个物品都有被挑选和不被挑选的情况,并且返回物品的总价值 ->f(i+1,j), f(i+1,j-w[i]) + v[i]
求最大值 ->Math.max(f(i+1,j), f(i+1,j-w[i]) + v[i])
static int n,W;
static int[] w=new int[100],v=new int[100];
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
n=scan.nextInt();
for(int i=0;i<n;i++) {
w[i]=scan.nextInt();
v[i]=scan.nextInt();
}
W=scan.nextInt();
//System.out.println(f1(0,0));
//System.out.println(f2(0,W));
solve1();
solve2();
}
//思路一
///第i个物品,当前重量为j+w[i]
static int f1(int i,int j) {
if(i==n) return 0;
//超重
else if(j+w[i]>W) return f1(i+1,j);
//主体
else return Math.max(f1(i+1,j), f1(i+1,j+w[i])+v[i]);
}
//思路二
//第i个物品,剩余可用重量为j
static int f2(int i,int j) {
//选到最后一个了
if(i==n) return 0;
//不能选择(超重)
else if(j<w[i]) return f2(i+1,j);
//主功能
else return Math.max(f2(i+1,j), f2(i+1,j-w[i])+v[i]);
}
1.记忆化搜索
只不过,这种方法的搜索深度是 n ,而且每一层的搜索都需要两次分支,最坏就需要 O (2n)的时间,当 n 比较大时就没办法解了。
如图所示,f 以(3,2)为参数调用了两次。如果参数相同,返回的结果也应该相同,于是第二次调用时已经知道了结果却向向浪费了计算时间。
让我们在这里把第一次计算时的结果记录下来,省略掉第二次以后的重复计算试试看。
//记忆化搜索一
static int f(int i, int j) {
//如果已经计算过则使用以前的数据
if(dp[i][j]>0) return dp[i][j];
int res;
if (i == n) res=0;
else if (j + w[i] > W) res=f(i + 1, j);
else res=Math.max(f(i + 1, j), f(i + 1, j + w[i]) + v[i]);
return dp[i][j]=res;
}
//记忆化搜索二参考思路二自己实现
这微小的改进能降低多少复杂度呢?对于同样的参数,只会在第一次被调用到时执行递归部分,第二次之后都会直接返回。参数的组合不过 nW 种,而函数内只调用2次递归, 所以只需要 O (nW) 的复杂度就能解决这个问题。只需略微改良,可解的问题的规模就可以大幅提高。这种方法一般被称为 记忆化搜索 。
2.动态规划
接下来,我们来仔细研究一下前面的算法利用到的这个记忆化数组。记
dp[i][j]
为根据 f 的定义,
从第 i 个物品开始挑选总重小子 j 时,总价值的最大值。于是我们有如下递推式
如上所示,不用写递归函数,直接利用递推式将各项的值计算出来,简单地用二重循环也可以解决这一问题。
//动态规划一
static void solve2(){
for(int i=n-1;i>=0;i--){
for(int j=0;j<=W;j++){
if(j+w[i]>W)
dp[i][j]=dp[i+1][j];
else
dp[i][j]=Math.max(dp[i+1][j], dp[i+1][j+w[i]]+v[i]);
}
}
System.out.println(dp[0][0]);
}
//动态规划二自己实现
这个算法的复杂度与前面相同,也是 O(nW) ,但是简洁了很多。
以这种方式一步步按顺序求出问题的解的方法被称作动态规划法,也就是常说的 DP 。解决问题时既可以按照如上方法从记亿化搜索出发推导出递推式,熟练、后也可以直接得出递推式。