目录
[1.1 算法原理讲解](#1.1 算法原理讲解)
[1.1.1 状态表示](#1.1.1 状态表示)
[1.1.2 状态转移方程](#1.1.2 状态转移方程)
[1.1.3 初始化](#1.1.3 初始化)
[1.1.4 填表顺序](#1.1.4 填表顺序)
[1.1.5 返回值](#1.1.5 返回值)
[1.2 代码实现](#1.2 代码实现)
[1.3 空间优化](#1.3 空间优化)
[2.1 算法原理讲解](#2.1 算法原理讲解)
[2.1.1 状态表示](#2.1.1 状态表示)
[2.1.2 状态转移方程](#2.1.2 状态转移方程)
[2.1.3 初始化](#2.1.3 初始化)
[2.1.4 填表顺序](#2.1.4 填表顺序)
[2.1.5 返回值](#2.1.5 返回值)
[2.2 代码实现](#2.2 代码实现)
[3.1 解法一](#3.1 解法一)
[3.1.1 状态表示](#3.1.1 状态表示)
[3.1.2 状态转移方程](#3.1.2 状态转移方程)
[3.1.3 初始化](#3.1.3 初始化)
[3.1.4 填表顺序](#3.1.4 填表顺序)
[3.1.5 返回值](#3.1.5 返回值)
[3.1.6 代码实现](#3.1.6 代码实现)
[3.2 解法二](#3.2 解法二)
[3.2.1 状态表示](#3.2.1 状态表示)
[3.2.2 状态转移方程](#3.2.2 状态转移方程)
[3.2.3 初始化](#3.2.3 初始化)
[3.2.4 填表顺序](#3.2.4 填表顺序)
[3.2.5 返回值](#3.2.5 返回值)
[3.2.6 代码实现](#3.2.6 代码实现)
1、第N个泰波那契数
1.1 算法原理讲解
动态规划就是创建1个一维数组或者二维数组作为DP表,填DP表,DP表中的某一项就是结果
动态规划的步骤通常分为5步
1.1.1 状态表示
是什么? dp表中每一项所代表的含义
怎么来? 1. 题目要求
-
经验 + 题目要求
-
分析问题的过程中,发现重复子问题
这道题是需要返回第n个泰斐波那契数列的值,所以dp[i]就表示第i个泰斐波那契数列的值
1.1.2 状态转移方程
动态规划的最终结果就是要将dp表填满,所以应该要知道dp[i]等于什么,即用之前的状态或之后的状态来表示当前的状态
状态转移方程就是dp[i]等于什么
这道题中dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
1.1.3 初始化
初始化是为了在填表的时候不会越界
这道题n是从0开始的,那么dp[0],dp[1],dp[2],放入状态转移方程中都会造成数值越界,所以需要对这几个值进行初始化
由题意可知,dp[0] = 0,dp[1] = dp[2] = 1
1.1.4 填表顺序
确定是从左向右开始填,还是从右向左开始填
依据就是,为了填写当前状态,所需要的状态已经计算过了
像这道题,填写当前状态,需要它的前三个状态已经填写过,所以是从左向右填
1.1.5 返回值
题目要求 + 状态表示
这道题就是dp[n]
1.2 代码实现
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] = dp[2] = 1;
for(int i = 3;i<=n;i++)
{
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
}
return dp[n];
}
};
1.3 空间优化
这道题中,当依次向后求dp[i]时,前面的有些状态是可以舍弃的,所以可以利用滚动数组优化
优化可以将空间复杂度O(N^2) -> O(N) , O(N) -> O(1)
class Solution {
public:
int tribonacci(int n) {
if(n == 0) return 0;
if(n == 1 || n == 2) return 1;
int a = 0,b = 1,c = 1,d = 0;
for(int i = 3;i<=n;i++)
{
d = a + b + c;
a = b;b = c;c = d;
}
return d;
}
};
2、三步问题
2.1 算法原理讲解
2.1.1 状态表示
dp[i]表示到达i位置一共有多少种方法
2.1.2 状态转移方程
第n个台阶可以从第n-1个台阶跨1步上去,也可以从第n-2个台阶跨2步上去,还可以从第n-3个台阶跨3步上去,所以dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
2.1.3 初始化
这道题n是从1开始的,那么dp[1],dp[2],dp[3],放入状态转移方程中都会造成数值越界,所以需要对这几个值进行初始化
2.1.4 填表顺序
这道题,填写当前状态,需要它的前三个状态已经填写过,所以是从左向右填
2.1.5 返回值
返回dp[n]
2.2 代码实现
class Solution {
public:
int waysToStep(int n) {
const int MOD = 1000000007;// 即le9 + 7
//处理边界条件
if(n == 1 || n == 2) return n;
if(n == 3) return 4;
vector<int> dp(n + 1);
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;
//注意这里每加一个就要取模,否则可能前两个相加就超过了int的最大范围
return dp[n];
}
};
注意:题目中说结果可能很大,所以每加1次就要取一次模
3、使用最小花费爬楼梯
3.1 解法一
3.1.1 状态表示
dp[i]表示到达第i阶台阶所需要的最小花费
3.1.2 状态转移方程
第i阶台阶是从第i - 1阶或第i - 2阶台阶上来的,所以到达第i阶台阶所需要的最小花费就等于到达i-1阶台阶的最小花费加上i-1阶台阶的花费、i-2阶台阶的最小花费加上i - 2阶台阶的花费,这二者中的较小值
dp[i] = min(dp[i - 1] + cost[i - 1] , dp[i - 2] + cost[i - 2])
3.1.3 初始化
需要对dp[0]和dp[1]初始化,因为可以选择从下标为 0 或 1 的元素作为初始阶梯
所以,dp[0] = dp[1] = 0
3.1.4 填表顺序
从左向右
3.1.5 返回值
dp[n]
3.1.6 代码实现
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
// 解法一:dp表的第i项表示到第i阶台阶所需要的体力值
int n = cost.size();
vector<int> dp(n + 1);// dp表的第i项表示到第i阶台阶所需要的体力值
dp[0] = dp[1] = 0;// 因为可以选择从第1阶或第2阶开始走
for(int i = 2;i<=n;i++)
{
dp[i] = min(dp[i - 1] + cost[i - 1],dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
};
3.2 解法二
3.2.1 状态表示
dp[i]表示从第i项到顶部所需要的最小花费
3.2.2 状态转移方程
从第i阶台阶可以向上走1步或者2步,所以从第i阶台阶到顶部的最小花费就等于这一步台阶的花费,加上从第i + 1阶台阶到顶部的最小花费或从第i + 2阶台阶到顶部的最小花费中的较小值
dp[i] = min(dp[i - 1] , dp[i - 2]) + cost[i]
3.2.3 初始化
需要对dp[n - 1]和dp[n - 2]初始化
dp[n - 1] = cost[n - 1],dp[n - 2] = cost[n - 2]
3.2.4 填表顺序
从右向左
3.2.5 返回值
返回dp[0]或dp[1]中的较小值,因为可以选择从下标为 0 或 1 的元素作为初始阶梯
3.2.6 代码实现
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
// 解法二:dp表的第i项表示从第i阶台阶到顶层所需要的体力值
int n = cost.size();
vector<int> dp(n);
dp[n - 1] = cost[n - 1],dp[n - 2] = cost[n - 2];
for(int i = n - 3;i>=0;i--)
{
dp[i] = min(dp[i + 1],dp[i + 2]) + cost[i];
}
return min(dp[0],dp[1]);
}
};
4、解码方法
dp[i]表示当前位置有几种解码方法
根据题目的意思,0不能单独组成一个编码,也不能在与其他字符组成编码时位于首位
此时需要分两种情况讨论:
- s[i]可以自身解码,即s[i] != '0'
若s[i]还能和s[i - 1]共同解码,即s[i]和s[i - 1]组成的数字范围在[10,26],dp[i] = dp[i - 1] + dp[i - 2]
若s[i]不能和s[i - 1]共同解码,dp[i] = dp[i - 1]
- s[i]不能自身解码,即s[i] == '0'
若s[i]还能和s[i - 1]共同解码,即s[i]和s[i - 1]组成的数字范围在[10,26],dp[i] = dp[i - 2]
若s[i]不能和s[i - 1]共同解码,dp[i] = 0
通过上面可知,为了防止溢出,我们需要对dp[0]和dp[1]初始化
-
当s[0] == '0',dp[0] = dp[1] = 0
-
当s[0] != '0',dp[0] = 1,若dp[1]无法自身解码,也无法和dp[0]共同解码,则dp[1] = 0,若dp[1]可以自身解码,也可以和dp[0]共同解码,则dp[1] = 2,若dp[1]可以自身解码和dp[1]可以于dp[0]共同解码只有一个为真,则dp[1] = 1
返回值就是dp[n - 1]
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
if(n == 1) return s[0] == '0'? 0 : 1;
vector<int> dp(n);//dp是记录当前位置一共有几种解码方法
if(s[0] == '0')// s[0]无法自身解码
{
dp[0] = dp[1] = 0;// 当s[0] == '0',则下标1处的解码方法数为0,因为0后面加一个数是解码失败的
}
else// s[0]可以自身解码
{
dp[0] = 1;
int x = (s[0] - '0') * 10 + s[1] - '0';// 记录s[0] 和 s[1]组成的数字是多少
if(s[1] == '0' && x >27) dp[1] = 0;// s[1]无法自身解码,也无法与s[0]共同解码
else if(s[1] != '0' && x >= 10 && x <= 26) dp[1] = 2;//s[1]可以自身解码,也可与s[0]共同解码
else dp[1] = 1;// s[1]只能自身解码或者与s[0]共同解码
}
for(int i = 2;i<n;i++)
{
if(s[i] == '0')// 自身无法解码
{
int x = (s[i - 1] - '0') * 10 + s[i] - '0';
if(x >= 10 && x <= 26) dp[i] = dp[i - 2];// 可以与前一个共同解码
else dp[i] = 0;// 无法与前一个共同解码
}
else// 自身可以解码
{
int x = (s[i - 1] - '0') * 10 + s[i] - '0';
if(x >= 10 && x <= 26) dp[i] = dp[i -1] + dp[i - 2];// 可以与前一个共同解码
else dp[i] = dp[i - 1];// 无法与前一个共同解码
}
}
return dp[n - 1];
}
};