动态规划核心思想:铭记历史或重蹈覆辙(功不唐捐)
故事引入:
A * “1+1+1+1+1+1+1+1 =?” *
A : “上面等式的值是多少”
B : 计算 “8!”
A *在上面等式的左边写上 “1+” *
A : “此时等式的值为多少”
B : quickly “9!”
A : “你怎么这么快就知道答案了”
A : “只要在8的基础上加1就行了”
A : “所以你不用重新计算因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间”
动态规划算法有两种规划方式:自顶向下的备忘录法 和 自底向上
以Fibonacci为例说明这两种方法:
Fibonacci 指的是f(n)满足: (n为自然数)
f (n) = 1 ,(n <= 1)
f (n) = f(n-1) + f(n-2) ,(n>1)
使用递归来实现这个算法:
public int fib (int n) {
if (n <= 1) return 1;
return fib (n-1) + fin(n-2);
}
分析递归流程:假设输入数6,那么执行的递归树为
上面的递归树中的每个子节点都会执行一次,很多重复的节点被执行,fib(2)被重复执行了5次。
由于调用每一个函数的时候都要保留上下文,所以空间上开销也不小。
这么多的子节点被重复执行,如果在执行的时候把执行过的子节点保存起来,后面要用到的时候直接查表调用的话可以节约大量的时间。
下面就看看动态规划的两种方法怎样来解决斐波拉契数列Fibonacci 数列问题。
-
自顶向下的备忘录法
public class Fibonacci { public static int getFib (int n) { if (n <= 1) return 1; int []memo = new int[n+1];//备忘录 for (int i=2; i < n+1; i++){ memo[i] = -1; } memo[0] = 1; memo[1] = 1; return fib(n, memo); } public static int fib(int n, int[] memo){ //递归出口 if (memo[n] != -1) return memo[n]; //递归 return fib(n-1, memo) + fib(n-2, memo); } public static void main(String[] args){ while (true) { Scanner sc = new Scanner(System.in); int n = Integer.parseInt(sc.nextLine()); System.out.println(getFib(n)); } } }
-
自底向上的动态规划(非递归,性能优于备忘录方法)
利用数组进行结果存储,不断为下一个值做准备public static int getFib2(int n){ if (n <= 1) return 1; int[] memo = new int[n+1]; memo[0] = 1; memo[1] = 1; for (int i=2; i <= n; i++) memo[i] = memo[i-1] + memo[i-2]; return memo[n]; }
-
以上即为动态规划的两种方式入门
动态规划例题:钢条切割
下面讲一下,该题求最大获利的三种方式:
-
递归求解
import java.util.Scanner; public class Cutting { public static int cut (int []p, int n) { if (n == 0) return 0;//递归出口,切分极致(无法再切分) int q = p[n];//不做任何切分时的价格 for (int i=1; i <= n; i++){ q = Math.max(q, p[i] + cut(p, n-i)); } return q; } public static void main (String[] args) { while(true){ Scanner sc = new Scanner(System.in); int n = Integer.parseInt(sc.nextLine());//输入长度 //根据题意,有价格长度一共有10种 int len = 10;//标记长度-价格 if (n > 10) len = n; int[] p = new int[len+1];//p用于存储不同长度的价格,从1开始计数 //(除了1-10有价格,其余价格都为0,不影响后续计算) p[1] = 1; p[2] = 5; p[3] = 8; p[4] = 9; p[5] = 10; p[6] = 17; p[7] = 17; p[8] = 20; p[9] = 24; p[10] = 30; System.out.println(cut(p, n)); } } }
-
备忘录求解
备忘录方法是对纯递归方法的一种优化,把每次递归中计算的最优解进行保存import java.util.Scanner; public class Cutting { public static int cut (int []p, int n) { if (n == 0) return 0;//递归出口,切分极致(无法再切分) if (p[n] > 0) return p[n];//注意:备忘录方法与纯递归的区别在于记忆 int q = p[n];//不做任何切分时的价格 for (int i=1; i <= n; i++){ q = Math.max(q, p[i] + cut(p, n-i)); } p[n] = q;//备忘录记忆 return q; } public static void main (String[] args) { while(true){ Scanner sc = new Scanner(System.in); int n = Integer.parseInt(sc.nextLine());//输入长度 //根据题意,有价格长度一共有10种 int len = 10;//标记长度-价格 if (n > 10) len = n; int[] p = new int[len+1];//p用于存储不同长度的价格,从1开始计数 //(除了1-10有价格,其余价格暂为0,后续备忘录记忆变更) p[1] = 1; p[2] = 5; p[3] = 8; p[4] = 9; p[5] = 10; p[6] = 17; p[7] = 17; p[8] = 20; p[9] = 24; p[10] = 30; System.out.println(cut(p, n)); } } }
-
自低向上求解
import java.util.Scanner; public class Cutting { public static int cut (int []p, int n) { if (n <= 10) return p[n];//0-10的最大利润已存在 p 中 for (int i=11; i <= n; i++) { int q = 0;//q用于记录利润 for (int j=1; j <= i; j++) { q = Math.max(q, p[j] + p[i-j]); } p[i] = q; } return p[n]; } public static void main (String[] args) { while(true){ Scanner sc = new Scanner(System.in); int n = Integer.parseInt(sc.nextLine());//输入长度 //根据题意,有价格长度一共有10种 int len = 10;//标记长度-价格 if (n > 10) len = n; int[] p = new int[len+1];//p用于存储不同长度的价格,从1开始计数 //(除了1-10有价格,其余价格都为0,不影响后续计算) p[1] = 1; p[2] = 5; p[3] = 8; p[4] = 9; p[5] = 10; p[6] = 17; p[7] = 17; p[8] = 20; p[9] = 24; p[10] = 30; System.out.println(cut(p, n)); } } }
什么时候适合使用动态规划?
-
最优子结构
若一个问题的解包含其子问题的最优解,即为最优子结构 -
重叠子问题
反复求解相同的子问题,不断重复计算
动态规划经典模型:
1. 线性模型
例题:在一个夜黑风高的晚上,有n(n <= 50)个小朋友在桥的这边,现在他们需要过桥,但是由于桥很窄,每次只允许不大于两人通过,他们只有一个手电筒,所以每次过桥的两个人需要把手电筒带回来,i号小朋友过桥的时间为T[i],两个人过桥的总时间为二者中时间长者。问所有小朋友过桥的总时间最短是多少。
贪心算法误区: 看到此题时,第一想法是让 n个人中过桥耗费时间最短的人往复,
送其他人渡桥。但到后面发现这种想法还是太年轻了!
假设现在有4个人要过桥,渡桥时间分别为 1,2,5,10。分别标记为 A、 B、 C、 D
如果让 A 分别送其余3人过桥的最终时间为 10 + 1 + 5 + 1 + 2 = 19
但实际做法应该这样时间比较短:
step1: A送B过河, 来回耗时 1 + 2 = 3
step2: CD过河,B回来,耗时 10 + 2 = 12
step3: AB过河,耗时 2
共计耗时17,因此让耗时最低的人来回送其他人渡桥的方法行不通
那该如何进行求解?
将桥划分为左右两端,分别标记为L和R,现在我们要将L端n人送到R端。将n个人的渡桥时间从小到大,从数组空间1开始进行排列,数组为arr,长度为 n+1(除去0号空间干扰)
假设前i个人最短渡桥时间为 opt[i],
考虑前 i-1个人已渡桥,余下1个人未渡桥的情况:
此时,手电筒必然在R端,所以 op[i] = op[i-1] + a[1] + a[i] (即最短耗时a[1]从R端渡桥接走a[i]最长耗时者,op[i-1]为i-1人渡桥的最短耗时)
那么如果L端现在余下的是2人未渡桥呢,又该如何解决?
同理,此时手电筒必然在R端,1. 耗时最低的a[1]先带手电筒渡桥到L端,渡桥耗时a[1]; 2. 原先L端余下两人的渡桥时间必然都比a[1]要长,故让这两人先渡桥,a[1]留守L端,渡桥耗时a[i];3. 让R端此时耗时最低的a[2]过桥来接a[1],来回合计耗时2a[2]。即op[i] = op[i-2] + a[1] + a[i] + 2a[2]
所以 op[i] = min(opt[i-1] + a[1] + a[i], op[i-2] + a[1] + a[i] + 2*a[2]),由此可进行动态规划
2. 区间模型
3. 背包模型
由于篇幅问题,区间模型和背包模型问题,后续会另开一篇博文详叙
注:本文转载并修改至博文 https://blog.csdn.net/u013309870/article/details/75193592