冻龟算法系列之斐波那契数列模型
文章目录
- 【动态规划】斐波那契数列模型
- 1. 第N个泰波那契数
- 1.1 题目解析
- 1.2 算法原理
- 1.2.1 状态表示
- 1.2.2 状态转移方程
- 1.2.3 初始化
- 1.2.4 填表顺序
- 1.2.5 返回值
- 1.3 编写代码
- 1.4 空间优化
- 2. 三步问题
- 2.1 题目解析
- 2.2 算法原理
- 2.2.1 状态表示
- 2.2.2 状态转移方程
- 2.2.3 初始化
- 2.2.4 填表顺序
- 2.2.5 返回值
- 2.3 编写代码
- 3. 使用最小花费爬楼梯
- 3.1 题目解析
- 3.2 算法原理
- 3.2.1 状态表示
- 3.2.2 状态转移方程
- 3.2.3 初始化
- 3.2.4 填表顺序
- 3.2.5 返回值
- 3.3 编写代码
- 3.4 解法2:以某节点为起点
- 3.4.1 得到状态转移方程
- 3.4.2 初始化
- 3.4.3 编写代码
- 4. 解码方法
- 4.1 题目解析
- 4.2 算法原理
- 4.2.1 状态表示
- 4.2.2 状态转移方程
- 4.2.3 初始化
- 4.2.4 填写顺序
- 4.2.5 返回值
- 4.3 编写代码
- 4.4 代码简化技巧
动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。
至于这个模型是什么意思,我觉得你往下看下去,自主感受那些相似之处,才不会懵逼~
而本文为动态规划系列的第一篇,所以在动态规划做题步骤上会比较分明,接下来的几道例题,让我们边实战边学吧!
文章解题的大的流程为:
传送门:力扣1137
题目:
所以有以下示例:
同理:
算法原理分析的过程分五步:
由于是第一道题,所以下面是细致讲解!
什么是状态表示:
这就涉及动态规划的核心:dp表
- 这个dp表的建立是不固定的,可能是一维数组,二维数组等等…
而dp表中的元素与其对应下标有什么关系,即这个元素的**含义**,或者说是这个元素所处的“状态”(抽象笼统的说法)
- 而其中,我们要的答案,就是dp表其中的一个元素
例子:
- 开心or不开心,是状态
- 1下标元素的含义就是滑稽老铁开心
- 当然,dp表代表的状态一般不是对立的,而是这样的
- 而之后我们可以 以“含义”去理解
而设立的状态表示不一样,也意味着思路不一样,即不同的解法~
- 想到一个,然后就去试试能不能解…
- 做多点题,这一步准确率会高点~
怎么来?
- 经验 + 题目要求
- 分析问题的过程中,发现重复的子问题,抽象成状态表示(暂时不讲)
而这道题,我们只需建立一个一维dp表(大小为n + 1):
即dp[i]表示:第 i 个泰波那契数的值
什么是**状态转移方程**:
- 就是,dp[i] = … ;
- 而这个方程可能有条件,分支,循环等复杂的逻辑…
而dp[i]是由dp表其他下标对应的值来推导出来的
- 比如,dp[i]之前或者之后的值可以推出dp[i]
例子:
- 开心是会传染的:
而本题的动态转移方程就是:
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
对应动态规划的题目而言,前两步是最最重要的,也就是百分之99已完成~
接下来就是一些细节的处理
本题为:
我们通过已知推未知,这里的已知可能是i之前或者i之后,这就分为了简单的两个大方向(一维dp表)
本题为:顺序1
根据题目要求 + 状态表示得出返回值
本题为:dp[n]
编写代码分为固定的四步:
class Solution {
public int tribonacci(int n) {
//1. 创建dp表
int[] dp = new int[n + 1];
//2. 初始化
if(n == 0) {
return 0;
}
if(n == 1 || n == 2) {
return 1;
}
dp[0] = 0;
dp[1] = 1;
dp[2] = 1;
//3. 填表
for(int i = 3; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
//4. 返回值
return dp[n];
}
}
在提交之前测试一下,可以自行输入实例去测~
可见空间复杂度和时间复杂度都为O(N)
通过“滚动数组”去进行空间优化
算法修改思想:
class Solution {
public int tribonacci(int n) {
//1. 空间优化
int[] src = new int[3];
src[0] = 0;
src[1] = 1;
src[2] = 1;
//2. 初始化
if(n == 0) {
return 0;
}
if(n == 1 || n == 2) {
return 1;
}
int ret = 0;
//3. 填表
for(int i = 3; i < n + 1; i++) {
ret = src[0] + src[1] + src[2];
src[0] = src[1];
src[1] = src[2];
src[2] = ret;
}
//4. 返回值
return ret;
}
}
则就从头部挪,而不是从尾部挪
从头部开始挪:
- 正常
从尾部开始挪:
- 异常,最终三个值都为2
第一道题就这样愉快的结束了,想必你已经了解了动态规划作答的流程了,接下来就是做几道题,走几遍流程,好好感受它“动归思想”~
传送门:面试题08.01.
题目:
输入一个数n,求出从0到n的爬法数
很显然,这要用到一维的顺序结构,dp表的大小为n + 1
题目要求:
经验:以某个节点为结尾或者以某个节点为起点去研究问题
得出dp表元素dp[i]的含义就是,到达第 i 个台阶的爬法数
而得出状态转移方程的思想就是:借用“已知”状态!
我们要到达 i,那么我们到达前的位置可以是:i - 1,i - 2,i - 3:
到达i - 1可以跳两步在跳一步或者跳一步再跳两步之类的啊
所以得出dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
可以通过示例验证一下想法:
同样的,i - 3 、i - 2、i - 1 需要考虑数组越界的问题~
dp[i]之前是已知,则填表应从左到右~
返回dp[n]~
一样的,分为四步:
class Solution {
public int waysToStep(int n) {
//1. 创建表
int[] dp = new int[n + 1];
int MOD = (int)1e9 + 7; //取模数
//2. 初始化,处理边界问题
if(n == 1) {
return 1;
}
if(n == 2) {
return 2;
}
if(n == 3) {
return 4;
}
dp[1] = 1;
dp[2] = 2;
dp[3] = 4;
//3. 填表
for(int i = 4; i < n + 1; i++) {
dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
}
//4. 返回值
return dp[n];
}
}
传送门:力扣746
题目:
输入一个cost数组,通过数组得到付费台阶数n,得到终点位置,输出到达终点的最小花费
很显然,这要用到一维的顺序结构
且dp数组的大小为n + 1
题目要求:返回到达终点的最小花费(理解为到达第n个台阶)
经验:以某一个节点为终点或者以某一个节点为起点去研究
所以状态表示就是:dp[i]代表从起点到 i 的最小消费
同样的,把dp[i]之前的值认为“已知”,以此为条件去推导dp[i]
我们要到达 i,那么我们到达前的位置可以是:i - 1,i - 2
而我们需要取其较小值~
所以得到,dp[i] = min{dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]}
对于0和1下标要进行一些初始化~
到达0和1并不需要支付,所以值为0~
由于是以i节点为终点,dp[i]之前的点为条件,所以顺序为左到右
返回dp[n],其代表第n个台阶(终点)的最小花费~
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
//1. 创建dp表
int[] dp = new int[n + 1];
//2. 初始化,处理边界
if(n == 0 || n == 1) {
return 0;
}
dp[0] = 0;
dp[1] = 0;
//3. 填表
for(int i = 2; i < n + 1; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
//4. 返回值
return dp[n];
}
}
得到状态表示为:dp[i] 代表 第i个台阶跳到终点的最小花费
我们认定从dp[i]以后的值为“已知”,利用它们得到结果,从第i个台阶到终点的接下来的一步,要么到达i + 1,要么到达i + 2:
并且取其较小值最为dp[i]
所以方程为:dp[i] = min{dp[i + 1], dp[i + 2]} + cost[i];
dp[n]为终点,值为0
dp[n - 1]到终点只有一步之遥,值为cost[n - 1]
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
//1. 创建dp表
int[] dp = new int[n + 1];
//2. 初始化,处理边界
if(n == 0 || n == 1) {
return 0;
}
dp[n] = 0;
dp[n - 1] = cost[n - 1];
//3. 填表
for(int i = n - 2; i >= 0; i--) {
dp[i] = Math.min(dp[i + 1], dp[i + 2]) + cost[i];
}
//4. 返回值
return Math.min(dp[0], dp[1]);
}
}
所以状态表示的不同,算法思路就有所不同,至于怎么选中呢?
传送门:力扣91
题目:
显然,也是一维的dp表(大小为n)
题目要求:输入字符串,返回整个字符串能反解出来的解的个数
经验:以某个节点为终点
那么我们趋向于让答案合理的为dp[n - 1]:
得到状态表示:dp[i]代表字符串[0, i]的子串最多能反解出来的解的个数
同样的,我们把dp[i]之前的值视为“已知”,把它们当做条件,去推导dp[i]
那么,要扩展到[0, i]的字符串,则扩展成[0, i]字符串之前,应该为[0, i - 1]的子串或者[0, i - 2]的子串:
得出状态转移方程:
dp[i] = (dp[i - 1]) + (dp[i - 2]);
对于0和1下标要特殊处理:
0的话
1的话
由于是以某个节点为终点,所以填写顺序为从左到右~
返回dp[n - 1],代表整个字符串能反解的解的个数
class Solution {
public int numDecodings(String s) {
int n = s.length();
char[] string = s.toCharArray();
//创建dp表
int[] dp = new int[n];
//初始化并处理边界问题
if(string[0] != '0') {
dp[0] = 1;
}
if(n == 1) {
return dp[0];
}
if(string[0] !='0' && string[1] != '0') {
dp[1]++;
}
int number = (string[0] - '0') * 10 + string[1] - '0';
if(number >= 10 && number <= 26) {
dp[1]++;
}
for(int i = 2; i < n; i++) {
if(string[i] != '0') {
dp[i] += dp[i - 1];
}
number = (string[i - 1] - '0') * 10 + string[i] - '0';
if(number >= 10 && number <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n - 1];
}
}
而且,跟填表操作还差不多:
而这个fake为多少呢,一般是为0的,但是一定要据实际而言!
所以新dp表的大小为n + 1,返回值为dp[n];
注意这里的dp[i]代表的是[0, i)的字符串子串!
class Solution {
public int numDecodings(String s) {
int n = s.length();
char[] string = s.toCharArray();
int[] dp = new int[n + 1];
dp[0] = 1;
if(string[0] != '0') {
dp[1] += 1;
}
if(n == 1) {
return dp[1];
}
for(int i = 2; i < n + 1; i++) {
if(string[i - 1] != '0') {
dp[i] += dp[i - 1];
}
int number = (string[i - 2] - '0') * 10 + string[i - 1] - '0';
if(number >= 10 && number <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
}
这个小优化能掌握是更好的~
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭!动态规划第一章结束啦,本章节讲解的类型都差不多,是不是有点“斐波那契”那味!已知推未知!牢记思想流程,特别重要
如果你理解的不错,那你很有天赋!
这动态规划的第一步走的不错!