
|--------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| 作者简介 | |
| 算法篇 | Linux篇 |
| QT百日筑基篇 | 数据结构篇 |
动态规划------斐波那契数列模型
目录/索引
目录
[1. 状态表示](#1. 状态表示)
[2. 状态转移方程](#2. 状态转移方程)
[3. 初始化](#3. 初始化)
[4. 填表顺序](#4. 填表顺序)
[5. 返回值](#5. 返回值)
[6. 细节问题](#6. 细节问题)
[1. 状态表示](#1. 状态表示)
[2. 状态转移方程](#2. 状态转移方程)
[3. 初始化](#3. 初始化)
[4. 填表顺序](#4. 填表顺序)
[5. 返回值](#5. 返回值)
[1. 状态表示:](#1. 状态表示:)
[2. 状态转移方程](#2. 状态转移方程)
[3. 初始化](#3. 初始化)
[4. 填表顺序](#4. 填表顺序)
[5. 返回值](#5. 返回值)
了解模型的概念:
我们的动态规划算法中有许多的模型形如路径问题,背包问题...。我们这次选择的就是一个斐波那契数列的模型。我们先回顾一下什么是斐波那契呢?
下面是斐波那契数列, 我们下面做的题就是使用了斐波那契数列的类似的通向公式。

解题的步骤:
我们一般会在一下的几个方面进行讲解:
状态表示
状态转移方程
初始化
填表顺序
返回值
题目一:第N个泰波那契数
题目链接:
题目解析:
在我们的题目中已将给了我们T0/1/2的值,这也就是我们固定的三个值,当然这一题也非常的友好直接给出了我们对应的公式,不要我们进行对应的推导。他的状态转移方程就是
还有一句话非常的友好答案保证是一个32位的(int)所以我们不要担心过度越界的问题。

算法原理:
我们以测试用例1来进行推导:

状态表示:
所以我们可以搞一个dp数组来进行对应位置的值储存(说人话: dp数组下标为4 就是储存T4 这个位置的泰波那契数) 即dpi=Ti;
状态转移方程:
当然我们得将前面公式进行对应的变形:++Tn=Tn-1 + Tn-2 + Tn -3;++
初始化:
我们的状态转移方程可得知:当我们的n==1时下表T-1已经越界。所以我们的初始话前三位这样能保证我们的数组不被越界。所以我们该怎么的进行初始化呢??? 回到题目他已经给了我们对应的优化了。

填表的顺序:
这里我们想要求出后面的必须知道前面的,所以填表顺序时从左向右(从前向后)
返回值:

直接返回dp【n】即可。
复杂度:
时间:O(n) 空间: O(n)
代码的编写:
class Solution {
public:
int tribonacci(int n)
{
//边界情况
if(n==0) return 0;
if(n==2 || n==1) return 1;
vector<int> dp(n+1);
//初始化
dp[0]=0;
dp[1]=dp[2]=1;
for(int i=3; i<dp.size();i++)
dp[i]=dp[i-1]+dp[i-2]+dp[i-3];//动态转移方程
return dp[n];
}
};
运行提交:

空间复杂度的优化------滚动数组
在这里我们有点小优化:
这里我们创建一个vector会有以下空间的浪费,我们回到公式Tn=Tn-1 + Tn-2 + Tn -3;我们发现对我们有用的好像就只有3个, 所以我们这里可以创建三个变量来进行操作。三个记录前面的值。一直的移动这个直。知道得到最后的值:
cpp
class Solution {
public:
int tribonacci(int n)
{
//边界情况
if(n==0) return 0;
if(n==2 || n==1) return 1;
int a,b,c,count;
a=0;
b=c=1;
for(int i=3; i<=n;i++)
{
count=a+b+c;//记录最终值
a=b;//将b的值给a
b=c;//将c的值给b
c=count;//将计算出的值给c
//继续循环滚动
}
return c;
}
};
这样的空间复杂度: O(n)-> O(1)运行:
提交

题目链接:
题目解析:
有n阶台阶,一个小孩可能走1,2,3步我们要实现它有多上中方法走到n。


算法原理:
1. 状态表示
假设n==3 那么小孩有几种可能? ,小孩可以先到2然后一步到3, 也可以走到1两步到3, 也可以在0三部到3.s所以我们到2这个位置的方法:dp2 同理: dp1dp0我们dpi表示在i阶台阶的方法总数。 所以我们能得到dp3=dp2+dp1+dp0。
2. 状态转移方程
dpi=dpi-1+dpi-2}dpi-3;
3. 初始化
我们能发现我们只需要dp2dp1dp0需要进行初始化,我们dp0=0(我就在0这个位置)
dp1=1(只有1步过去)dp2=2(1,1 / 2两种方法)。
4. 填表顺序
我们要得到前面的才能继续的得到后面的数所以是从左向右(从前向后)
5. 返回值
返回n位置的数就是n阶台阶的数。
6. 细节问题
数据太大可能会越界我们需要对计算结果进行取模。

编写代码:
cpp
class Solution {
public:
int waysToStep(int n)
{
const int MOD=1000000007;
vector<int> dp(n+1,0);
if(n==1) return 1;
if(n==2) return 2;
dp[1]=1;dp[2]=2;dp[3]=4;
for(int i=4; i<=n ;i++)
{
dp[i]=((dp[i-1]+dp[i-2])%MOD+dp[i-3])%MOD; //只在最后进行取模的话还是会越界报错
}
return dp[n];
}
};
运行:

空间优化:
cpp
class Solution {
public:
int waysToStep(int n)
{
const int MOD=1000000007;
if(n==1) return 1;
if(n==2) return 2;
int a,b,c,count;
a=1;b=2;c=4;
for(int i=4; i<=n ;i++)
{
count = ((a+b)%MOD+c)%MOD;
a=b;
b=c;
c=count;
}
return c;
}
};

题目链接:
题目解析:
我们每次花钱爬楼梯, 花钱都能爬1,2阶台阶。我们要进行计算出我们爬楼梯的最小的花费。

算法原理:
1. 状态表示
我们同样定义一个dp数组, dp数组存的是每一步所要的最小花费, 但是要注意的是它可以走两步或者走一部那么dpi位置需要知道dpi-1和dpi-2这啷个位置的哪个位置的花费更小一点, 加上当前位置的花费costi就得到了dpi位置的状态。
2. 状态转移方程
dpi=costi + dpi-1/dpi-2(这样两者的最小值)
3. 初始化
我们这里只要初始化dp前两位dp0=cost0
dp1=cost1这个是最低花费吗当然!我们如果一步一步走的话我们会多花费一个cost0的。
4. 填表顺序
我们知道了前面的才能计算后面的所以填表顺序是从前向后(从左向右)。
5. 返回值
返回最后一个位置/倒数第二个位置的最小值。
代码编写:
当然这里我是使用了三目运算, 调用库函数min也能完成这种的效果。
cpp
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost)
{
int n = cost.size();
vector<int> dp(n);
dp[0]=cost[0],dp[1]=cost[1];
for(int i=2;i<n;i++)
dp[i] = dp[i-1]>dp[i-2]?dp[i-2]+cost[i]:dp[i-1]+cost[i];
return dp[n-2]>dp[n-1]?dp[n-1]:dp[n-2];
}
};
运行结果:

题目链接:
题目解析:
我们的字符'1'-'26'表示我们的字符'A'-'Z' 给定我们一串字符串, 来让我们进行解码, 当然也有解码失败的情况: 形如06这样的默认为解码失败,如果一个空间只有一个0那么也没有对应的字母也算对应的解码失败,并且我们解码成功的标准是必须在字符1-26这个范围之内的。题目保证必须为32位.

算法原理:
1. 状态表示:
我们每次分为两个过程1. 自己一个进行解码, 2. 和前一个一起进行解码。当然这样、还有几种情况: 我们通过下图来进行深度的理解:

所以我们创建一个dp数组来进行储存当前位置的一个值成功的值。她可能是当前位置解码成功dpi=dpi-1(与前一个保持一致), 可能是与前一个一同解码成功dpi = dpi-2 (当前位置单独解码失败要和前面一起解码才算成功)可能是一起解码成功dpi=dpi-1+dpi-2;
2. 状态转移方程
1.她可能是当前位置解码成功dpi=dpi-1(与前一个保持一致), 、
2.可能是与前一个一同解码成功dpi = dpi-2 (当前位置单独解码失败要和前面一起解码才算成功)
3.可能是一起解码成功dpi=dpi-1+dpi-2;
3. 初始化
我们是这里要对前连个进行初始化, 当然这里还有点小细节的第一个位置, 当第一个位置为0时一定不会成功所以直接return, 如果!0那就将这里的值设置为dp0=1;
第二个位置如果只能单独解码那就设置为1, 能和前面的一起解码那就设置为2。如果不能进行合理的解码直接return
4. 填表顺序
我们要知道前面的数据才能进行后续的操作所以还是从左向右(从前向后)。
5. 返回值
直接返回最后的位置即可。
代码的编写:
cpp
class Solution {
public:
//动态规划
int numDecodings(string s)
{
if(s[0]=='0') return 0;
int n = s.size();
vector<int> dp(n);
//初始化
dp[0] = 1; // s[0]!=0
if(n==1) return dp[0];
if(s[1]>='1' && s[1]<='9') dp[1]+=dp[0];
int t = (s[0] - '0') * 10 + (s[1] - '0');
if(t>=10 && t<=26) dp[1]++;
for(int i=2;i<n;i++)
{
if(s[i] <= '9' && s[i] >= '1') dp[i] += dp[i - 1];
int t = (s[i - 1] - '0') * 10 + s[i] - '0';
if(t >= 10 && t <= 26) dp[i] += dp[i - 2];
}
return dp[n-1];
}
};
运行:

代码优化:
当我在写初始化的时候写了一边前面的初始化的过程, 然后在for循环中我们又写了一边相似的逻辑,所以是直接进行复制的所以我们可以将代码更加简洁一点。我们在初始化的时候写的有点数据的冗余所以我们可以采用多开一个空间的方法来进行对应的优化,当然我们对应的初始的数组还要进行一位的平移。
注意:
为什么dp0这个位置要置为1?
因为我们知道dp2这个位置答案为2, 所以直为1通过+dp1+dp0我们就能得到dp2=2;
有的时候需要置为0这里需要分情况来进行讨论。
cpp
class Solution {
public:
//动态规划
int numDecodings(string s)
{
if(s[0]=='0') return 0;
int n = s.size();
vector<int> dp(n+1);
//初始化
dp[0] = 1; // s[0]!=0
dp[1] = 1;
for(int i=2;i<n+1;i++)
{
if(s[i-1] <= '9' && s[i-1] >= '1') dp[i] += dp[i - 1];
int t = (s[i - 2] - '0') * 10 + s[i-1] - '0';
if(t >= 10 && t <= 26) dp[i] += dp[i - 2];
}
return dp[n];
}
};
运行:

本篇完

