动态规划入门--------------铭记历史或重蹈覆辙

动态规划核心思想:铭记历史或重蹈覆辙(功不唐捐)
故事引入:
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 数列问题。

  1. 自顶向下的备忘录法

     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));
             }
         }
     }
    
    1. 自底向上的动态规划(非递归,性能优于备忘录方法)
      利用数组进行结果存储,不断为下一个值做准备

       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];
       }
      

以上即为动态规划的两种方式入门

动态规划例题:钢条切割
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
下面讲一下,该题求
最大获利
的三种方式:

  1. 递归求解

     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));
             }
         }
     }
    
  2. 备忘录求解
    备忘录方法是对纯递归方法的一种优化,把每次递归中计算的最优解进行保存

     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));
             }
         }
     }
    
  3. 自低向上求解

     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. 最优子结构
    若一个问题的解包含其子问题的最优解,即为最优子结构

  2. 重叠子问题
    反复求解相同的子问题,不断重复计算

动态规划经典模型:
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

猜你喜欢

转载自blog.csdn.net/weixin_43247186/article/details/87185843