做导航网站怎么赚钱pinterest官网入口
目录
- 一、简介
 - 1.1 什么是动态规划?
 - 1.2 动态规划的两种形式
 - 1)自顶向下的备忘录法(记忆化搜索法)
 - 2)自底向上的动态规划
 - 3)两种方法对比
 
- 1.3 动态规划的 3 大步骤
 
- 二、小试牛刀:钢条切割
 - 2.1 题目描述
 - 2.2 题目解析
 - 1)第一步:定义数组元素的含义
 - 2)第二步:找出数组元素之间的关系
 - 3)第三步:找出初始值
 
- 2.3 最优子结构
 - 2.4 代码实现
 - 1)递归版本
 - 2)备忘录版本
 - 3)自底向上的动态规划
 
 
一、简介
1.1 什么是动态规划?
在说明动态规划前,我们先来了解一个小场景:
A: "1+1+1+1+1+1+1+1"A: "上面等式的值是多少?"
B: "(计算...)" "8!"A: "在上面等式的左边写上 '1+',此时等式的值为多少?"
B: "(立刻回答)" "9!"
A: "你这次怎么这么快就知道答案了"
B: "只要在8的基础上加1就行了"
 
由上面的小故事可知,动态规划 就是 通过记住历史的求解结果来节省时间 。
1.2 动态规划的两种形式
示例:斐波那契数列,又称黄金分割数列,其数值为:1、1、2、3、5、8、13、21、34,递推公式为:
  F ( 0 ) = 1 , F ( 1 ) = 1 , F ( n ) = F ( n − 1 ) + F ( n − 2 ) , n > 2 , n ∈ N ∗ F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2),n>2,n∈N^{*} F(0)=1,F(1)=1,F(n)=F(n−1)+F(n−2),n>2,n∈N∗
 这个算法用递归来实现非常简单,代码如下:
public int fib(int n) {if (n < 2) {return 1;}return fib(n - 1) + fib(n - 2);
}
 
先来分析一下递归算法的执行流程,假如输入 6,那么执行的递归树如下:

我们可以发现:
- 上面的递归树中,每一个结点都会执行一次;
 - 很多结点被重复执行。
 
为了避免这种情况,我们可以把执行过的结点值保存下来,后面用到直接查表,这样可以节省大量时间。
下面看下保存历史记录的两种形式:自顶向下的备忘录法、自底向上的动态规划。
1)自顶向下的备忘录法(记忆化搜索法)
备忘录法,也叫记忆化搜索法,是比较好理解的:
- 创建了一个 n+1 大小的数组来保存求出斐波那契数列中的每一个值;
 - 在递归的时候,如果发现之前已经算过了就不再计算;
 - 如果之前没有计算,则计算后放入历史记录中。
 
public static void main(String[] args) {int n = 6;// 声明数组,用于记录历史,初始化为-1int[] his = new int[n + 1];Arrays.fill(his, -1);System.out.println(fib(n, his));
}public static int fib(int n, int[] his) {if (n < 2) {return 1;}// 读取历史if (his[n] != -1) {return his[n];}int result = fib(n - 1, his) + fib(n - 2, his);// 记录历史his[n] = result;return result;
}
 
2)自底向上的动态规划
备忘录法还是利用了递归,不管怎样,当计算 fib(6) 的时候还是要去先计算出 fib(1) ~ fib(5),那么为何不先计算出 f(1) ~ f(5) 呢?这就是动态规划的核心:先计算子问题,再由子问题计算父问题。
public static int fib(int n) {int[] arr = new int[n + 1];arr[0] = 1;arr[1] = 1;for (int i = 2; i <= n; i++) {arr[i] = arr[i - 2] + arr[i - 1];}return arr[n];
}
 
自底向上的动态规划方法也是利用数组保存了计算的值,为后面的计算使用。
内存空间优化:
我们观察上面的代码会发现:参与循环的只有 fib(i)、fib(i-1)、fib(i-2) 项,因此该方法的空间可以进一步的压缩如下:
public static int fib(int n) {int num_i = 0;int num_i_1 = 1;int num_i_2 = 1;for (int i = 2; i <= n; i++) {num_i = num_i_2 + num_i_1;num_i_2 = num_i_1;num_i_1 = num_i;}return num_i;
}
 
3)两种方法对比
- 一般来说,由于备忘录的动态规划形式使用了递归,递归的时候会产生额外的开销,所以不推荐。
 - 相比之下,使用自底向上的动态规划方法要好些,也更容易理解。
 
1.3 动态规划的 3 大步骤
动态规划,无非就是利用 历史记录,来避免我们的重复计算。这些历史记录的存储,一般使用 一维数组 或 二维数组 来保存。
第一步:定义数组元素的含义
- 上面说了,我们用一个数组来保存历史数据,假设用一维数组 
dp[]来保存。这个时候有一个非常重要的点:如何规定数组元素的含义? 即dp[i]代表什么意思? 
第二步:找出数组元素之间的关系
- 动态规划类似于我们高中学习的 
数学归纳法。当我们要计算d[i]时,可以利用 dp[i-1]、dp[i-2] … dp[1] 来推导证明。 
第三步:找出初始值
- 学过 
数学归纳法的都知道,虽然知道了数组元素之间的关系式后,可以通过 dp[i-1] 和 dp[i-2] 来计算 dp[i],但是我们首先至少要知道dp[0]和dp[1]才能推导后面的值。dp[0] 和 dp[1] 就是所谓的初始值。 
二、小试牛刀:钢条切割
2.1 题目描述

2.2 题目解析
1)第一步:定义数组元素的含义
由题目可知:
p[]是价格数组,长度为i英寸的钢条价格为p[i];r[]是最大收益数组,长度为i英寸的钢条可以获得的最大收益为r[i];- 钢条的价格不确定,可能切割的收益更高,也可能不切割的收益更高。
 
通过解析可知,数组元素含义: 长度为 i 英寸的钢条可以获得的最大收益为 r[i]。
注意: 这里的 收益是指价格的总和,比如:2 英寸的钢条切割后收益为:1+1=2,相比之下不切割的 5 收益更高。
2)第二步:找出数组元素之间的关系
假如我们要对长度为 4 英寸的钢条进行切割,所有切割方案如下:

由图可见,我们将 r[4] 的计算转换成了 r[1]~ r[3] 的计算。
  r 4 = m a x ( r 1 + r 3 , r 1 + r 1 + r 2 , r 2 + r 2 , p 4 ) ; r_{4}=max(r_{1}+r_{3},r_{1}+r_{1}+r_{2},r_{2}+r_{2},p_{4}); r4=max(r1+r3,r1+r1+r2,r2+r2,p4);
 以此类推,可以继续转换 r[3]:
 
由图可见,我们继续将 r[3] 的计算转换成了 r[1]~r[2] 的计算。
  r 3 = m a x ( r 1 + r 2 , r 1 + r 1 + r 1 , p 3 ) r_{3}=max(r_{1}+r_{2},r_{1}+r_{1}+r_{1},p_{3}) r3=max(r1+r2,r1+r1+r1,p3)
 以此类推,可以继续转换 r[2]:
 
由于 1 英寸的钢条无法切割,所以 r[1]=p[1]。
  r 2 = m a x ( r 1 + r 1 , p 2 ) r_{2}=max(r_{1}+r_{1},p_{2}) r2=max(r1+r1,p2)
 由于 r[2] 中包含了 r[1] + r[1],那么 r[3] 中的:
  m a x ( r 1 + r 2 , r 1 + r 1 + r 1 ) = m a x ( r 1 + r 2 ) max(r_{1}+r_{2},r_{1}+r_{1}+r_{1})=max(r_{1}+r_{2}) max(r1+r2,r1+r1+r1)=max(r1+r2)
 由于 r[3] 中包含了 r[1] + r[2],那么 r[4] 中的:
  m a x ( r 1 + r 3 , r 1 + r 1 + r 2 ) = m a x ( r 1 + r 3 ) max(r_{1}+r_{3},r_{1}+r_{1}+r_{2})=max(r_{1}+r_{3}) max(r1+r3,r1+r1+r2)=max(r1+r3)
 所以整理 r[1]、r[2]、r[3]、r[4] 为:
  r 1 = p 1 r_{1}=p_{1} r1=p1
r 2 = m a x ( r 1 + r 1 , p 2 ) r_{2}=max(r_{1}+r_{1},p_{2}) r2=max(r1+r1,p2)
r 3 = m a x ( r 1 + r 2 , p 3 ) r_{3}=max(r_{1}+r_{2},p_{3}) r3=max(r1+r2,p3)
r 4 = m a x ( r 1 + r 3 , r 2 + r 2 , p 4 ) r_{4}=max(r_{1}+r_{3},r_{2}+r_{2},p_{4}) r4=max(r1+r3,r2+r2,p4)
根据公式进行递推, r[n] 为:
  r n = m a x ( r 1 + r n − 1 , r 2 + r n − 2 , . . . , r n / 2 + r n − n / 2 , p n ) r_{n}=max(r_{1}+r_{n-1},r_{2}+r_{n-2},...,r_{n/2}+r_{n-n/2},p_{n}) rn=max(r1+rn−1,r2+rn−2,...,rn/2+rn−n/2,pn)
3)第三步:找出初始值
其实初始值我们在第二步已经找出来了:
r[1]=p[1]=1r[2]=max(r[1]+r[1],p[2])=5
2.3 最优子结构
通过该题我们注意到,为了求规模为n的原问题,我们 先求解形式完全一样,但规模更小的子问题。当完成首次 切割后,我们 将两段钢条看成两个独立的钢条切割问题实例。我们 通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
我们称 钢条切割问题 满足 最优子结构 性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
2.4 代码实现
1)递归版本
递归很好理解,思路和回溯法是一样的,遍历所有解空间。但这里和上面斐波那契数列的不同之处在于:这里在每一层上都进行了一次最优解的选择,q=Math.max(q, p[i]+cut(n-i)); 这段代码就是选择最优解。
final static int[] p = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30};public static int cut(int n) {if (n == 0) {return 0;}int max = Integer.MIN_VALUE;for (int i = 1; i <= n; i++) {max = Math.max(max, p[i - 1] + cut(n - i));}return max;
}
 
2)备忘录版本
备忘录方法无非是在递归的时候记录下已经调用过的子函数的值。钢条切割问题的经典之处在于自底向上的动态规划问题的处理,理解了这个也就理解了动态规划的精髓。
public static int cutByHis(int n) {int[] p = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30};int[] r = new int[n + 1];for (int i = 0; i <= n; i++) {r[i] = -1;}return cut(p, n, r);
}public static int cut(int[] p, int n, int[] r) {int q = -1;if (r[n] >= 0)return r[n];if (n == 0)q = 0;else {for (int i = 1; i <= n; i++)q = Math.max(q, cut(p, n - i, r) + p[i - 1]);}r[n] = q;return q;
}
 
3)自底向上的动态规划
自底向上的动态规划问题中最重要的是要理解在子循环遍历中的 i 变量,相当于上面两个方法中的 n 变量,i-j 主要用于获取历史计算过的问题值。
final static int[] p = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30};public static int cutByDP(int n) {int[] r = new int[n + 1];for (int i = 1; i <= n; i++) {int q = -1;for (int j = 1; j <= i; j++)q = Math.max(q, p[j - 1] + r[i - j]);r[i] = q;}return r[n];
}
 
整理完毕,完结撒花~ 🌻
参考地址:
1.算法-动态规划 Dynamic Programming–从菜鸟到老鸟,https://blog.csdn.net/u013309870/article/details/75193592
2.告别动态规划,连刷40道动规算法题,我总结了动规的套路,https://blog.csdn.net/hollis_chuang/article/details/103045322
