1、动态规划定义:
动态规划是分治思想的延伸,和递归类似,却又不是递归。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
2、 动态规划具备以下三个特点:
1) 把原来的问题分解成了几个相似的子问题
2) 所有的子问题都只需要解决一次
3) 储存子问题的解
3、 动态规划的本质是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)动态规划问题一般从以下四个角度考虑:
1) 状态定义
2) 状态间的转移方程定义
3) 状态的初始化
4) 返回结果
把握以上四个角度,问题就迎刃而解了。
状态定义的要求:定义的状态一定要形成递推关系(一步一步走)
4、 适用场景:最大值/最小值,可不可行,存不存在,是不是,方案个数。
5、举个栗子:
例题Ⅰ:斐波那契数列
题目要求:输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。n<=39
大家一看到这道题,脑海里肯定会想到:斐波那契?简单啊,递归不就行了。非也非也~
首先来看递归的做法:
public class Solution {
public int Fibonacci(int n) {
if(n == 0) return 0;
if(n < 3) return 1;
else {
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
}
f(n) = f(n-1) + f(n-2),确实第一眼看就是递归啊,简直完美的递归环境,这样想着,两三行代码就搞定了。
但其实这样写,n稍微大一点很容易产生Stack Overflow。为什么会溢出呢?因为这其中存在着大量的重复计算。以n=4为例,因为代码并没有记录Fibonacci(1)和Fibonacci(0)的结果,对于程序来说它每次递归都是未知的,因此光是n=4时f(1)就重复计算了3次之多。所以就很容易产生Stack Overflow。
用动态规划来看:
初始状态:F[0] = 0 ; F[1] = F[2] = 1 ;
转移方程:F[i] = F[i - 1] + F[i - 2];
返回值:F[n];
public class Solution {
public int Fibonacci(int n) {
if(n == 0) return 0;
if(n < 3) return 1;
else {
int[] f = new int[n + 1];
f[0] = 0;
f[1] = f[2] = 1;
for(int i = 3;i <= n; ++i){
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
}
}
这样就相当于定义了一个数组来存放F[n],当进行下一步操作时,可以直接调用数组里F[n]的数值,省去了大量的重复计算。
题目要求:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
推导过程:
分析以后代码就很好写了:
public class Solution {
public int JumpFloorII(int target) {
if (target <= 0) {
return -1;
} else if (target == 1) {
return 1;
} else {
return 2 * JumpFloorII(target - 1);
}
}
}
以上两道题帮助大家理解动态规划,如果着实搞不清楚动态规划的话,推荐一个动态规划的短漫画来加强理解:
什么是动态规划?
看完之后你肯定会觉得任督二脉已打通,动态规划不过如此!