动态规划(Dynamic Programming),因此常用 DP 指代动态规划。
动态规划解题思路:
(1)确定「DP 状态」:「最优子结构」 和 「无后效性」
(2)确定「DP 转移方程」。
下面通过 宝石挑选 问题来介绍动态规划。
(一)宝石挑选问题
1.问题引入
小 Q 是一个宝石爱好者。
这一天,小 Q 来到了宝石古董店,店家觉得小 Q 是个宝石行家,于是决定和小 Q 玩一个游戏。
游戏是这样的,一共有n 块宝石,每块宝石在小 Q 心中都有其对应的价值。注意,由于某些宝石质量过于差劲,因此存在只有店家倒贴钱,小 Q 才愿意带走的宝石,即价值可以为负数。
小 Q 可以免费带走一个连续区间中的宝石,比如区间 [1,3] 或区间 [2,4] 中的宝石。
请问小 Q 能带走的最大价值是多少?
2.问题分析
(1)暴力破解
枚举所有区间,暴力累加区间中宝石的价值,最后选一个价值最大的区间。时间复杂度 O(n3)。
显然有些无法接受,因此想想有没有办法优化,比如优化掉暴力累加的部分。
(2)优化 1.0
仔细思考不难发现,我们可以枚举区间右端点,然后固定右端点,左端点不断向左移动,边移动边累加,就可以将时间复杂度优化到 O(n2)。
例如我们固定右端点是 3,那么左端点就从 3 移动到 1,边移动边累加答案,就可以在移动过程中计算出区间 [3 ,3]、 [2, 3]、 [1, 3]的答案了。因此枚举所有区间右端点,即可在 O(n2)时间复杂度内找到答案。
但是O(n2) 时间还是有些过高了,还有没有办法继续优化呢?
(3)优化 2.0
观察O(n2) 的解法,不难发现我们用了O(n) 的时间复杂度才求出了固定某个点为区间右端点时,区间最大价值和。
例如固定了n为区间右端点后,我们通过从 n 到1枚举左端点,才求出了以n为区间右端点时的区间最大价值和,即O(n)的时间复杂度。
那么继续思考,「以n为区间右端点的区间最大和」,与「以n-1为区间右端点的区间最大和」,这两者是否有关联呢?
为了描述方便,接下来我们用 f[i] 来代替「以i为区间右端点的区间最大和」,用a[i]来代替第i块宝石的价值。
不难发现,如果f[n-1] 为正数,则 f[n] = f[n-1]+a[n]
如果 f[n-1] 为负数,则 f[n] = a[n]。根据上面我们可以推导出以下转移方程:f[i] = max( f[i-1] + a[i], f[i])
根据上述转移方程,我们可以在O(n) 时间复杂度内求出最大的f[i],即将此题时间复杂度优化到 O(n),而这个优化的过程就是「动态规划」的过程。
(二)动态规划概述
在上述宝石挑选问题的推导过程中,一共分为两步:
1. 将整个问题划分为一个个子问题,并令 f[i] 为第 i 个子问题的答案
2. 思考大规模的子问题如何从小规模的子问题推导而来,即如何由 f[i-1] 推出 f[i]
这两个步骤是「动态规划」解题思路的核心所在,即确定动态规划时的「DP状态」与「DP转移方程」。
1.DP状态
(1)最优子结构
「DP 状态最优值由更小规模的 DP 状态最优值推出」
将原有问题化分为一个个子问题,即为子结构。而对于每一个子问题,其最优值均由「更小规模的子问题的最优值」推导而来,即为最优子结构。
因此「DP 状态」设置之前,需要将原有问题划分为一个个子问题,且需要确保子问题的最优值由「更小规模子问题的最优值」推出,此时子问题的最优值即为「DP 状态」的定义。
在「宝石挑选」例题中,原有问题是「最大连续区间和」,子问题是「以i为右端点的连续区间和」。并且子问题「以i为右端点的最大连续区间和」由更小规模的子问题「以i-1为右端点的最大连续区间和」推出,因此满足「最优子结构」原则。
由此我们才定义 DP 状态 f[i] 表示子问题的最优值,即「以i为右端点的最大连续区间和」。
(2)无后效性
「无论 DP 状态是如何得到的,都不会影响后续 DP 状态的取值」
「无后效性」
顾名思义,就是我们只关心子问题的最优值,不关心子问题的最优值是怎么得到的。
以「宝石挑选」例题为例,我们令 DP 状态 f[i] 表示「以i为右端点的最大连续区间和」,我们只关心「以i为右端点的区间」这个子问题的最优值,并不关心这个子问题的最优值是从哪个其它子问题转移而来。
即无论f[i]所表示区间的左端点是什么,都不会影响后续f[i+1] 的取值。影响f[i+1] 取值的只有f[i] 的数值大小。
「有后效性」
我们对「宝石挑选」例题增加一个限制,即小 Q 只能挑选长度 <=K 的连续区间。此时若我们定义 f[i]表示「以i为右端点的长度 <=K的最大连续区间和」,则f[i+1] 的取值不仅取决于f[i] 的数值,还取决于f[i] 是如何得到的(即是如何挑选 <=k 的最大连续区间)。
因为如果f[i] 取得最优值时区间长度 =K,则f[i+1] 不能从f[i] 转移得到,即f[i] 的状态定义有后效性。
2.DP 转移方程
有了「DP 状态」之后,我们只需要用「分类讨论」的思想来枚举所有小状态向大状态转移的可能性即可推出「DP 转移方程」。
以「宝石挑选」问题为例,在我们定义「DP 状态」f[i] 之后,我们考虑状态f[i] 如何从f[1]~f[i-1]这些更小规模的状态转移而来。
仔细思考可以发现,由于f[i] 表示的是连续区间的和,因此其取值只与f[i-1]有关,与 f[1]~f[i-2]均无关。
我们再进一步思考,f[i] 取值只有两种情况,一是向左延伸,包含f[i-1],二是不向左延伸,仅包含a[i],由此我们可以得到下述「DP 转移方程」:
f[i] = max( f[i-1] + a[i], f[i]) //( 1<=i<=n, f[0]=0 )
下面是leetcode里动态规划的例题
(三)三步问题
有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模 1000000007。
示例 1:
输入:n = 3
输出:4
说明: 有四种走法
示例 2:
输入:n = 5
输出:13
数据范围
n 范围在 [1, 1000000] 之间
解题思路:
1.最优子结构
此题需要求出爬 n 阶楼梯的总方案数,因此很容易想到子问题是爬 i 阶楼梯的总方案数。
令f[i] 表示爬i阶楼梯的总方案数,原问题被划分为了多个求最优值的子问题,继续思考,不难发现小孩爬楼梯只有三种选项,一次上 1、2、3 阶,因此f[i] 的值仅由f[i-1]、f[i-2]、f[i-3] 的值决定,因此符合「最优子结构」原则。
2.无后效性
f[i]的取值与f[i-1]、f[i-2]、f[i-3] 的数值是如何得到的无关,因此符合「无后效性」原则。
3.转移方程
由于小孩只有三种爬楼选项,因此f[i] 的值仅由f[i-3]~f[i-1] 决定,且由于爬楼的最后一步不同,因此f[i] 的值由 f[i-3]~f[i-1] 累加得到,即上i楼梯的方式共有以下:
f[i] = ( f[i-1] + f[i-2] + f[i-3] ) % mod //f[1] = 1
4.C++代码
class Solution {
public:
int waysToStep(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
if(n == 3) return 4;
vector<int> sum;
sum.resize(n+1); //开辟多一个,为了从下标1开始,更直观
sum[1] = 1; sum[2] = 2; sum[3] = 4;
for(int i = 4; i <= n; ++i){
//直接套公式,但不要下成下面这种形式,前面的累加太大了
//sum[i] = (sum[i-1] + sum[i-2] + sum[i-3]) % 1000000007 %1000000007;
//而是每加一次就 % mod
sum[i] = sum[i-1]; //跨1阶
sum[i] = (sum[i] + sum[i-2]) % 1000000007; //跨2阶
sum[i] = (sum[i] + sum[i-3]) % 1000000007; //跨3阶
}
return sum[n];
}
};
(四)最小路径和问题
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。(说明:每次只能向下或者向右移动一步。)
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
1.最优子结构
此题需要求出从左上角出发,到达坐标 (m, n) 的路径数字和最小值。因此不难想到,子问题就是从左上角出发,到达坐标(i, j)的路径数字和最小值。
令f[i][j]表示从左上角到坐标 (i, j) 的路径数字和最小值,原问题即可被划分为多个求最优值的子问题,且由于每次只能向下或向右移动一步,因此f[i][j]的取值由f[i-1][j]和f[i][j-1]的值决定,即符合「最优子结构原则」。
2.无后效性
f[i][j]的取值与f[i-1][j]和f[i][j-1]所对应的路径数字和最小值是怎样得到的无关,因此符合「无后效性」。
如果题目改为每次可以向上、下、左、右移动一步,且不能走重复的格子,则f[i][j]的值虽然与 f[i][j-1]、f[i][j+1]、f[i-1][j]、f[i+1][j]的值有关,但由于「不能走重复的格子」这一限制,f[i][j-1]~f[i+1][j]所对应的具体路径会影响到f[i][j]的取值,即不符合「无后效性」。
3.转移方程
由于只能向下或向右移动一步,且由于其最后一步不同,因此f[i][j]由f[i-1][j]和f[i][j-1]中的最小值转移得到,即如下所示:
f[i][j] = min (f[i-1][j], f[i][j-1]) + grid[i][j]
//grid[i][j]表示坐标[i][j]处的数字大小,f[1][1] = grid[1][1]
4.C++代码
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
for(int i = 0; i < grid.size(); i++) //向下步
for(int j = 0; j < grid[0].size(); j++) { //向右步
if(i == 0 && j == 0) continue;
else if(i == 0) grid[i][j] = grid[i][j-1] + grid[i][j];//上边界,只能从左边来
else if(j == 0) grid[i][j] = grid[i-1][j] + grid[i][j];//左边界,只能从上边来
else grid[i][j] = min(grid[i][j-1], grid[i-1][j]) + grid[i][j];
}
return grid[grid.size()-1][grid[0].size()-1];
}
};
(五)最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成
解题思路
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果去除首尾两个字母,“bab” 仍是回文串,相同的,“bab”前后加上两个相同的字母的话,它仍然是个回文串。
根据这样的思路,我们就可以用动态规划的方法解决本题。我们用 P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串(下文表示成 s[i:j])是否为回文串:
P(i,j) = true, 如果子串Si ~ Sj 是回文串
= false, 其它情况
这里的「其它情况」包含两种可能性:
s[i, j] 本身不是一个回文串;
i > j,此时 s[i, j]本身不合法。
那么我们就可以写出动态规划的状态转移方程:
P(i, j) = P(i+1, j-1)∧(Si == Sj)
也就是说,只有 s[i+1:j-1]是回文串,并且 s 的第 i 和 j 个字母相同时,s[i:j]才会是回文串。
上文的所有讨论是建立在子串长度大于 2 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 1 或 2。对于长度为 1 的子串,它显然是个回文串;对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
P(i, i) = true
P(i, i+1) = (Si == Si+1)
根据这个思路,我们就可以完成动态规划了,最终的答案即为所有 P(i, j) = true 中 j-i+1(即子串长度)的最大值。
注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
C++代码:
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n));
string ans;
for (int l = 0; l < n; ++l) {
for (int i = 0; i + l < n; ++i) {
int j = i + l;
if (l == 0) {
dp[i][j] = 1; //i = j的情况,一个
} else if (l == 1) {
dp[i][j] = (s[i] == s[j]); //j = i + 1,两个
} else {
dp[i][j] = (s[i] == s[j] && dp[i + 1][j - 1]); //两个以上
}
if (dp[i][j] && l + 1 > ans.size()) {
ans = s.substr(i, l + 1);
}
}
}
return ans;
}
};
个人公众号:拾一札记
参考:
作者:力扣(LeetCode)
链接:https://leetcode-cn.com/
链接:https://www.zhihu.com/question/39948290/answer/1309260344
如果文章有错别字,或者内容有错误,或其他的建议和意见,请您联系我指正,非常感谢!!