目录
动态规划dp算法原理
动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
与分治法不同的是,适合于用动态规划求解的问题,经分解得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上)。
(除此斐波那契dp外还有其它类型的dp在后面会更新。)
动态规划算法解决问题的分类:
计数 | 有多少种方式走到右下角 / 有多少种方法选出k个数使得和是sum |
求最大值/最小值 | 从左上角走到右下角路径的最大数字和最长上升子序列长度 |
求存在性 | 取石子游戏,先手是否必胜 / 能不能取出k 个数字使得和是 sum |
动态规划dp算法一般步骤:
- 确定状态表示(dp[ i ] 表示什么,一般以 i 位置为起点或结尾分析,化成子问题)
- 状态转移方程(斐波那契数列的状态转移方程为:dp[i] = dp[i-1] + dp[i-2])
- 初始化(斐波那契数列初始化可以为dp[0] = 0, dp[1] = 1;)
- 填表顺序(斐波那契数列从左往右填)
- 返回值(如果斐波那契数列要求是第 n 个斐波那契数,返回dp[ n ] 即可)
①力扣1137. 第 N 个泰波那契数
难度 简单
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n
,请返回第 n 个泰波那契数 Tn 的值。
示例 1:
输入:n = 4 输出:4 解释: T_3 = 0 + 1 + 1 = 2 T_4 = 1 + 1 + 2 = 4
示例 2:
输入:n = 25 输出:1389537
提示:
0 <= n <= 37
- 答案保证是一个 32 位整数,即
answer <= 2^31 - 1
。
class Solution {
public:
int tribonacci(int n) {
}
};
解析代码1
简单的DP,根据题目已经得到状态转移方程了:
class Solution {
public:
int tribonacci(int n) {
if(n <= 1) // 处理边界
return n;
vector<int> dp(n+1, 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];
}
};
解析代码2
滚动数组对解法1进行空间上的优化,后面类似的空间优化就不写了,因为笔试没用,面试能讲出来就行。
class Solution {
public:
int tribonacci(int n) {
if(n <= 1) // 处理边界
return n;
int a = 0, b = 1, c = 1, d = 1; // 滚动数组思想优化空间
for(int i = 3; i <= n; ++i)
{
d = a + b + c;
a = b;
b = c;
c = d;
}
return d;
}
};
②力扣面试题 08.01. 三步问题
难度 简单
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
示例1:
输入:n = 3 输出:4 说明: 有四种走法
示例2:
输入:n = 5 输出:13
提示:
- n范围在[1, 1000000]之间
class Solution {
public:
int waysToStep(int n) {
}
};
解析代码
和上一题力扣1137. 第 N 个泰波那契数状态转移方程一样,就是要注意数据溢出。
class Solution {
public:
int waysToStep(int n) {
const int MOD = 1e9 + 7;
if(n <= 2) // 处理边界
return n;
vector<int> dp(n+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];
}
};
③力扣746. 使用最小花费爬楼梯
难度 简单
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20] 输出:15 解释:你将从下标为 1 的台阶开始。 - 支付 15 ,向上爬两个台阶,到达楼梯顶部。 总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1] 输出:6 解释:你将从下标为 0 的台阶开始。 - 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。 - 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。 - 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。 - 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。 - 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。 - 支付 1 ,向上爬一个台阶,到达楼梯顶部。 总花费为 6 。
提示:
2 <= cost.length <= 1000
0 <= cost[i] <= 999
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
}
};
解析代码1
如果dp[ i ]表示到达i位置的最小花费,则到达dp[ i ] 就有以下两种情况:
- 先到达dp[ i-1 ] 然后 支付cost[ i-1] 到达 dp[ i ]。
- 先到达dp[ i-2 ] 然后 支付cost[ i-2] 到达 dp[ i ]。
题意及取两种情况小的那个,得到状态转移方程:dp[i] =min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n + 1, 0); // dp[i]表示到达i位置的最小花费
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]; // 楼顶是原数组最后位置的下一个
}
};
解析代码2
如果dp[ i ]表示从 i 位置出发,到达楼顶,此时的最小花费,则dp[ i ] 就有以下两种情况:
- 支付 cost[ i ],往后走一步,从 i + 1的位置出发到终点
- 支付 cost[ i ],往后走两步,从 i + 2的位置出发到终点
则需要知道 i + 1 和 i + 2 位置的最小花费,及dp [i + 1] 和 dp[i + 2]。所以要从右往左填
则状态转移方程为:dp[i] = min(dp[i+1] + cost[i], dp[i+2] + cost[i]);
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n = cost.size();
vector<int> dp(n, 0); // dp[i]表示到达i位置的最小花费
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] + cost[i], dp[i+2] + cost[i]);
}
return min(dp[0], dp[1]);
}
};
④力扣91. 解码方法
难度 中等
一条包含字母 A-Z
的消息通过以下映射进行了 编码 :
'A' -> "1" 'B' -> "2" ... 'Z' -> "26"
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106"
可以映射为:
"AAJF"
,将消息分组为(1 1 10 6)
"KJF"
,将消息分组为(11 10 6)
注意,消息不能分组为 (1 11 06)
,因为 "06"
不能映射为 "F"
,这是由于 "6"
和 "06"
在映射中并不等价。
给你一个只含数字的 非空 字符串 s
,请计算并返回 解码 方法的 总数 。
题目数据保证答案肯定是一个 32 位 的整数。
示例 1:
输入:s = "12" 输出:2 解释:它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入:s = "226" 输出:3 解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
示例 3:
输入:s = "06" 输出:0 解释:"06" 无法映射到 "F" ,因为存在前导零("6" 和 "06" 并不等价)。
提示:
1 <= s.length <= 100
s
只包含数字,并且可能包含前导零。
class Solution {
public:
int numDecodings(string s) {
}
};
解析代码1
dp[i]表示字符串中区间的总编码方法。
关于 i 位置的编码状况,可以分为下面两种情况:
- 让 i 位置上的数单独解码成一个字母
- 让 i 位置上的数与 i - 1 位置上的数结合,解码成一个字母
让 i 位置上的数单独解码成一个字母,就存在 解码成功 和 解码失败 两种情况:
- 解码成功:dp[i] = dp[i - 1];([0,i] 上所有解码结果后面填上一个字母即可)
- 解码失败:dp[i] = 0;(前面努力都白费)
让 i 位置上的数与 i - 1 位置上的数结合在⼀起,解码成一个字母,也存在 解码成功 和 解码失败 两种情况:
- 解码成功:dp[i] = dp[i - 2]; 原因同上。
- 解码失败:dp[i] = 0;(前面努力都白费)
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
vector<int> dp(n, 0); // dp[i]表示字符串中[0,i]区间,总编码方法
dp[0] = (s[0] != '0'); // 题目只有0-9,不等于0就能单独编码
if(n == 1)
return dp[0];
if(s[0] != '0' && s[1] != '0') // 两个都能单独编码
dp[1] += 1;
int y = (s[0] -'0') * 10 + (s[1] - '0');
if(y >= 10 && y <= 26) // 和在一起能编码,不含前导0
dp[1] += 1;
for(int i = 2; i < n; ++i)
{
if(s[i] != '0') // 能单独编码
dp[i] += dp[i-1];
y = (s[i-1] -'0') * 10 + (s[i] - '0');
if(y >= 10 && y <= 26) // 和在一起能编码,不含前导0
dp[i] += dp[i-2];
}
return dp[n-1];
}
};
解析代码2
解析代码1是直接初始化,有点麻烦,在其基础上可以添加辅助位置初始化,注意两个点:
- 辅助结点里面的值要保证后续填表是正确的
- 下标的映射关系(此题下标有的要减1)
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
vector<int> dp(n+1, 0); // 辅助结点简化
dp[0] = 1;
dp[1] = (s[0] != '0');
for(int i = 2; i <= n; ++i)
{
if(s[i-1] != '0') // 能单独编码
dp[i] += dp[i-1];
int y = (s[i-2] -'0') * 10 + (s[i-1] - '0');
if(y >= 10 && y <= 26) // 和在一起能编码,不含前导0
dp[i] += dp[i-2];
}
return dp[n];
}
};
本篇完。
下一篇是链表的OJ,下下篇是动态规划的另一种类型:路径dp。