看这个笔记,比价通俗易懂地介绍了动态规划有道云笔记
经典的01背包问题
有一个包和n个物品,包的承重为v,每个物品都有各自的重量和价值,问当从这n个物品中选择多个物品放在包里而物品总重量不超过包的承重v时,能够得到的最大价值是多少?[对于每个物品不可以取多次,最多只能取一次,之所以叫做01背包,0表示不取,1表示取]
输入:物品的重量序列w,价值序列p,包的承重v
递归
这个问题首先可以用递归来解决:
对于最后一个物品,我们有选或者不选两种决策,1)选:价值为p[n]+m(n-1,v=v-w[n]),即当前物品的价值加上扣除当前物品重量后前n-1个物品的最优解 2)不选:我们将v全部分配给前n-1个物品,求最优解,m(n-1,v).
所以递归式为:
m(n,v) = max(m(n-1,v-w[n])+p[n] , m(n-1,v))
边界为:
n=0时,如果v能装下w[0]则返回p[0]否则返回0
n<0或者v<0,返回0
如果v连当前元素都装不下,直接将v分配给前n-1个物品
根据上面这个递归式,我们就可以写出完整的递归代码:
/**
*
* @param w
* 重量表
* @param p
* 价值表
* @param volumn
* 背包的最大承重
* @return 不超过最大承重的情况下所能装载物品的最大价值总和
*/
public static int recursion(int[] w, int[] p, int volumn) {
return recursion(w, p, w.length - 1, volumn);
}
private static int recursion(int[] w, int[] p, int index, int volumn) {
if (index < 0 || volumn <= 0) {
return 0;
}
if (index == 0 && volumn >= w[0]) {
return p[0];
}
if (index == 0 && volumn < w[0]) {
return 0;
}
if (volumn < w[index]) {
return recursion(w, p, index - 1, volumn);
} else {
return Math.max(recursion(w, p, index - 1, volumn), // 不选
recursion(w, p, index - 1, volumn - w[index]) + p[index]); // 选
}
}
}
但是这份代码在我的电脑上执行,100个元素执行时间为recursion持续时间:46844毫秒
int[] w = Util.getRandomArr(100, 1, 50);
int[] p = Util.getRandomArr(100, 1, 30);
int total = 100;
Instant now = Instant.now();
System.out.println(recursion(w, p, total));
System.out.println("recursion持续时间:" + (Instant.now().toEpochMilli() - now.toEpochMilli()) + "毫秒");
效率很低,因为我们重复计算了。
我们定义前x项分配y重量的xy为一个状态,这个状态对应着一个子问题,这个子问题在递归过程中将会被多次求解。
我们一共有2^n个子问题(可能重复),就要求解2^n次。
接下来,我们就要考虑如何进行改进,我们自然而然就可以想到如果每算出一个状态的解就保存起来,下次用到其值的时候直接取用,则可免去重复计算。
这里可以想到有x*y即n*v个状态,那么可以用n²的时间复杂度和一个辅助的二维数组来完成,比指数函数的效率要高很多。
记忆递归型
根据这个思路,我们就可以将上面的代码进行改进,使之成为记忆递归型的动态规划程序:
public static int recursion_m(int[] w, int[] p, int volumn) {
int[][] state = new int[w.length][volumn + 1];
return recursion_m(w, p, w.length - 1, volumn, state);
}
private static int recursion_m(int[] w, int[] p, int index, int volumn, int[][] state) {
if (index < 0 || volumn <= 0) {
return 0;
}
if (index == 0 && volumn >= w[0]) {
return p[