@TOC
先说一下动态规划斐波拉契数列模型的解决常见办法:
这类问题常见的解法就是以 i 位置为结尾,然后按照题目的题解总结状态转移方程,在进行基础的初始化,通过已有数据,推导出未来某一时刻的相关数据。总结如下:
1. 状态表示
2. 状态转移方程
3. 初始化
4. 填表顺序
5. 返回值
接下来通过几个题来进行讲解:
第N个泰波那契数
这是动态规划最基础的一道题
链接 : . - 力扣(LeetCode)
代码讲解:
按照我们上面说的步骤来进行讲解:
状态表示:
这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]
状态转移方程:
这个题的状态转移方程, 题目已经是给我们说明了 dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
初始化
题目上面的也是有提到 dp[0] = 0, dp[1] = 1, dp[2] = 1
填表顺序
我们要知道第n个泰波那契数, 就必须知道n - 1, n - 2, n - 3, 依次类推, 顺序是从左往右逐渐推导
返回值
我们需要的是第n个泰波那契数, 那么返回值自然是dp[n];
代码展示:
class Solution {
public:
int tribonacci(int n) {
if(n == 0)
{
return 0;
}
if(n == 1 || n == 2)
{
return 1;
}
vector<int> dp(n + 1);
dp[0] = 0, dp[1] = 1, dp[2] = 1;
for(int i = 3; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
return dp[n];
}
};
三步问题
链接 : . - 力扣(LeetCode)
代码讲解:
状态表示:
这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]
状态转移方程:
这里的转移方程我们需要分析一下:
假如是 i 个台阶, 想要上到第 i 个台阶一共有三种方法:
从 i - 3的台阶处,一口气走上来
从 i - 2的台阶处, 一口气走俩台阶上来
从i - 1的台阶处, 一步上来
因此 dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
初始化
假如是一个台阶 : 那么就是一个一步到位 , 即 : dp[0] = 1
假如是两个台阶 : 那么就是两种方法 : 一种是从一步一个台阶上来,一种是直接一步两个台阶上来 , 即 dp[1] = 2
假如是三个台阶,那么就有很多种方法 : 一步一个台阶上去, 一步三个台阶上去, 先一步一个台阶,然后一步两个台阶,后者先一步两个台阶, 然后一步一个台阶. 即 dp[2] = 4;
填表顺序
既然是爬楼梯,自然是从左向右,依次推导
返回值:
dp[0] 表示 1 个台阶, dp[1] 表示2 个台阶, n个台阶就是dp[n - 1] . 因此返回值是dp[n - 1]
代码展示:
class Solution {
public:
int waysToStep(int n) {
if(n == 1)
{
return 1;
}
if(n == 2)
{
return 2;
}
if(n == 3)
{
return 4;
}
vector<long long> dp(n);
dp[0] = 1, dp[1] = 2, dp[2] = 4;
for(int i = 3; i < n; i++)
{
dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % 1000000007;
}
return dp[n - 1] % 1000000007;
}
};
注意 : 结果可能会超出 int 的限制, 所以dp建议使用long long, 如果还会超出就是用题目中说明的1000000007
使用最小花费爬楼梯
链接 : . - 力扣(LeetCode)
代码讲解:
方法一:
状态表示:
这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]
状态转移方程
我们创建的dp数组, 我们希望每个元素都是到达该阶梯最小的花费,而走楼梯的时候,我们可以选择走一个台阶或者两个台阶. dp[i] = min(dp[i - 1] + cost[i], dp[i - 2] + cost[i - 2])
初始化:
0 号和1 号阶梯前面没有花费,登上他们花费的体力和cost[i]相等
因此只需要初始化dp[0], dp[1]
填表顺序:
想知道dp[i]的花费,需要知道dp[i - 1]和dp[i - 2], 依次类推,肯定是从左向右的
返回值
自然是返回dp[n]
方法二:
状态表示:
这个题我们就是创建一个dp数组,每一个状态就是以i 为结尾的dp[i]
状态转移方程
这里我们需要的是从i到最后还需要花多少力气,那么越往后,还需要花费的力气越少,到了最后的n - 1, n -2位置的时候,直接使用该位置需要的力气就可以了,因此dp[i] =min(dp[i + 1] , dp[i + 2]) + cost[i];
初始化
和方法一相反,需要知道最后两个位置的花费
返回值
选择dp[0], dp[1]之中的最小值,因为到达这俩位置不花费力气,只需要知道这俩位置到最后谁花的少
代码展示
方法一
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size() + 1);
for(int i = 2; i <= cost.size(); i++)
{
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[cost.size()];
}
};
方法二
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n);
dp[n - 1] = cost[n - 1];
dp[n - 2] = cost[n - 2];
for(int i = dp.size() - 3; i >= 0; i--)
{
dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];
}
return min(dp[0], dp[1]);
}
};
解码方法
代码讲解:
状态表示:
我们创建一个dp数组,这个数组,每一个元素表示从0到当前位置的解码方法。每一个dop[i]就是当前所需数据的状态
状态转移方程:
字母A-Z,26个字母中,有的只需要一个下标就可以定位,有的需要两个下表才能定位,因此这个状态转移方程需要分情况说明
首先是解码的成功和失败,假如解码失败,那么直接变成0,
失败的情况有很多,以下进行列举
- 当字符串的首元素为0, 那么这串字符串就无法进行解码,结果为0
- 当字符串在进行单一下标的解码的时候,如果出现了 1<= a <= 9 之外的字符的时候,解码失败,结果为0
- 当字符串在进行两个下标的解码的时候,如果出现 10 <= s <= 26, 那么解码失败,结果为0
假如成功就会分成两个情况
- 当以一个下标来进行解码, 那么解码成功出来的结果种类数就是dp[i - 1]
- 当以两个下标来进行解码,那么解码成功出来的结果种类就是dp[i - 2]
状态转移方程就是 dp[i] = dp[i - 1] + dp[i - 2]
初始化
要解码从0 到 i 位置的字符串,必须知道dp[i - 1] 和 dp[i - 2],那么依次类推,dp[0]和dp[1]是必须初始化的
返回值
返回的是dp[i]
代码展示
class Solution {
public:
bool OneIsOK(const char s)
{
int number = s - '0';
if(number >= 1 && number <= 9)
{
return true;
}
return false;
}
bool DoubleAreOK(string s)
{
if(s[0] == '0')
{
return false;
}
int number = stoi(s);
if(number >= 10 && number <= 26)
{
return true;
}
return false;
}
int numDecodings(string s) {
if(s[0] == '0')
{
return 0;
}
if(s.size() == 1)
{
return 1;
}
int n = s.size();
vector<int> dp(n);
string sub;
//设置两个函数
//判断单一位置是否有效
if(OneIsOK(s[0]))
{
dp[0]++;
}
if(OneIsOK(s[1]))
{
dp[1]++;
}
//判断该位置和前面的位置一起是否有效
sub = s.substr(0, 2);
if(DoubleAreOK(sub))
{
dp[1]++;
}
for(int i = 2; i < dp.size(); i++)
{
if(OneIsOK(s[i]))
{
dp[i] += dp[i - 1];
}
sub = s.substr(i - 1, 2);
if(DoubleAreOK(sub))
{
dp[i] += dp[i - 2];
}
}
return dp[n - 1];
}
};
通过这个代码很容易发现,这个动态规划初始化 dp[0]和dp[1]的时候非常的繁琐,而且·可以看到在初始化dp[1]的时候,和后续的推导其实差不多,那到底有没有什么办法,让dp[1]和后面的dp[i]一起推导出来,答案是有的————虚拟节点
优化:
我们在前面加一个节点dp[-1], 通过dp[-1]和dp[0]来推导出来后面的数据
注:这里的dp[-1]是为了清晰,才这么讲,实际上应该是dp[0],s[i] 等效到dp数组就是dp[i +1]
那么dp[-1]到底应该是一个怎么样的值呢
我们看前面的状态转移方程的推导,当以一个下标来进行解码, 那么解码成功出来的结果种类数就是dp[i - 1], 因此假如第一个位置解码成功了,那么他的解码方法数就是dp[-1],dp[0] 我们知道是1,因此dp[-1]肯定是1
因此这个代码的优化就是
class Solution {
public:
bool OneIsOK(const char s)
{
int number = s - '0';
if(number >= 1 && number <= 9)
{
return true;
}
return false;
}
bool DoubleAreOK(string s)
{
if(s[0] == '0')
{
return false;
}
int number = stoi(s);
if(number >= 10 && number <= 26)
{
return true;
}
return false;
}
int numDecodings(string s) {
if(s[0] == '0')
{
return 0;
}
int n = s.size();
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i < dp.size(); i++)
{
if(OneIsOK(s[i - 1]))
{
dp[i] += dp[i - 1];
}
string sub = s.substr(i - 2, 2);
if(DoubleAreOK(sub))
{
dp[i] += dp[i - 2];
}
}
return dp[n];
}
};