前言
动态规划(Dynamic Programming,简称 DP)是一种解决多阶段决策过程最优化问题的方法。它是一种将复杂问题分解成重叠子问题的策略,通过维护每个子问题的最优解来推导出问题的最优解。
动态规划的主要思想是利用已求解的子问题的最优解来推导出更大问题的最优解,从而避免了重复计算。因此,动态规划通常采用自底向上的方式进行求解,先求解出小规模的问题,然后逐步推导出更大规模的问题,直到求解出整个问题的最优解。
动态规划通常包括以下几个基本步骤:
- 定义状态:将问题划分为若干个子问题,并定义状态表示子问题的解;
- 定义状态转移方程:根据子问题之间的关系,设计状态转移方程,即如何从已知状态推导出未知状态的计算过程;
- 确定初始状态:定义最小的子问题的解;
- 自底向上求解:按照状态转移方程,计算出所有状态的最优解;
- 根据最优解构造问题的解。
动态规划可以解决许多实际问题,例如最短路径问题、背包问题、最长公共子序列问题、编辑距离问题等。同时,动态规划也是许多其他算法的核心思想,例如分治算法、贪心算法等。
动态规划是一种解决多阶段决策过程最优化问题的方法,它将复杂问题分解成重叠子问题,通过维护每个子问题的最优解来推导出问题的最优解。动态规划包括定义状态、设计状态转移方程、确定初始状态、自底向上求解和构造问题解等步骤。动态规划可以解决许多实际问题,也是其他算法的核心思想之一。
一、最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
1.1、思路
假设 nums 数组的长度是 n,下标从 0 到 n−1。
用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,那么要求的答案就是:
时间复杂度 O(n)、空间复杂度 O(n) 的实现,即用一个 f 数组来保存 f(i) 的值,用一个循环求出所有
f(i)。考虑到 f(i) 只和 f(i−1) 相关,于是我们可以只用一个变量 pre 来维护对于当前 f(i) 的 f(i−1) 的值是多少,从而让空间复杂度降低到 O(1),这有点类似「滚动数组」的思想。
1.2、代码实现
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0, maxAns = nums[0];
for (const auto &x: nums) {
pre = max(pre + x, x);
maxAns = max(maxAns, pre);
}
return maxAns;
}
};
二、跳跃游戏
给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
来源:力扣(LeetCode)。
2.1、思路
状态表示:令f[i]表示是否可以到达i。
递推分析:当考虑f[i]的可达情况时,可以发现,如果i可达,那么一定是从前面的某个可达的点j直接跳过去的,并且j i之间的距离小于等于j对应的步数的值,如果i之前存在这样可以直接跳过去的点,就说明i可达,否则i不可达,于是我们遍历一下i之前所有的点即可。
递推公式:
f[i] = (f[i - 1] && nums[i - 1] >= 1) || (f[i - 2] && nums[i - 2] >= 2)......|| (f[0] && nums[0] >= i)
初始f[0] = true,然后从前往后按序递推即可。
优化:注意到,如果i不可达,那么i之后的所有点也必然不可达,因为i之前的所有点最远都到达不了i, 那必然也到达不了i之后的点。时间复杂度虽然还是 O ( n 2 ) O(n^2) O(n2)但是避免了很多重复计算。
2.2、代码实现
class Solution {
public:
bool canJump(vector<int>& nums) {
int l = nums.size();
vector<bool> f(l);
f[0] = true;
for(int i = 1; i < l; i ++)
{
for(int j = i - 1; j >= 0; j --)
if(f[j] && nums[j] >= i - j)
{
f[i] = true;
break;
}
//如果i不可达,则直接退出循环,i后面的点都不可达
if(!f[i]) break;
}
return f[l - 1];
}
};
小结:用dp[i] 表示能否到达i。 比如 [2,3,1,1,4], 初始化dp[0] = true, 考虑 第一个元素 2,那么 dp[1] =true, dp[2] = true; 遍历下一个元素3 ; 3 可到达,那么dp[2] = true dp[3] = true dp[4] = true,依次类推。
三、解码方法
一条包含字母 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” 并不等价)。
来源:力扣(LeetCode)。
3.1、思路
对于给定的字符串 s,设它的长度为 n,其中的字符从左到右依次为 s[1],s[2],⋯,s[n]。可以使用动态规划的方法计算出字符串 s 的解码方法数。
fi 表示字符串 s 的前 i 个字符 s[1…i] 的解码方法数。在进行状态转移时,我们可以考虑最后一次解码使用了 s 中的哪些字符,那么会有下面的两种情况:
- 第一种情况是我们使用了一个字符,即 s[i] 进行解码,那么只要 s[i]≠0,它就可以被解码成 A∼I 中的某个字母。由于剩余的前 i−1 个字符的解码方法数为 f (i−1) ,因此可以写出状态转移方程:
- 第二种情况是我们使用了两个字符,即 s[i−1] 和 s[i] 进行编码。与第一种情况类似,s[i−1] 不能等于 0,并且 s[i−1] 和 s[i] 组成的整数必须小于等于 26,这样它们就可以被解码成 J∼Z 中的某个字母。由于剩余的前 i−2 个字符的解码方法数为 f (i−2) ,因此可以写出状态转移方程:
3.2、代码实现
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
// a = f[i-2], b = f[i-1], c = f[i]
int a = 0, b = 1, c;
for (int i = 1; i <= n; ++i) {
c = 0;
if (s[i - 1] != '0') {
c += b;
}
if (i > 1 && s[i - 2] != '0' && ((s[i - 2] - '0') * 10 + (s[i - 1] - '0') <= 26)) {
c += a;
}
tie(a, b) = {b, c};
}
return c;
}
};
时间复杂度:O(n)。
空间复杂度:O(1)。
总结
动态规划(Dynamic Programming)是一种解决多阶段决策最优化问题的方法,它将复杂问题分解成重叠子问题并通过维护每个子问题的最优解来推导出问题的最优解。动态规划可以解决许多实际问题,例如最短路径问题、背包问题、最长公共子序列问题、编辑距离问题等。
动态规划的基本思想是利用已求解的子问题的最优解来推导出更大问题的最优解,从而避免了重复计算。它通常采用自底向上的方式进行求解,先求解出小规模的问题,然后逐步推导出更大规模的问题,直到求解出整个问题的最优解。
动态规划通常包括以下几个基本步骤:
- 定义状态:将问题划分为若干个子问题,并定义状态表示子问题的解;
- 定义状态转移方程:根据子问题之间的关系,设计状态转移方程,即如何从已知状态推导出未知状态的计算过程;
- 确定初始状态:定义最小的子问题的解;
- 自底向上求解:按照状态转移方程,计算出所有状态的最优解;
- 根据最优解构造问题的解。
动态规划的时间复杂度通常为 O ( n 2 ) O(n^2) O(n2)或 O ( n 3 ) O(n^3) O(n3),空间复杂度为O(n),其中n表示问题规模。在实际应用中,为了减少空间复杂度,通常可以使用滚动数组等技巧来优化动态规划算法。