@(Aaron) [LeetCode, C++]
主要内容包括:
-
动态规划
-
实例讲解
1、什么是动态规划
动态规划(dynamic programming,简称 dp)是一种多阶段决策最优解模型,一般用来求最值问题,多数情况下它可以采用自下而上的递推方式来得出每个子问题的最优解(即最优子结构),进而自然而然地得出依赖子问题的原问题的最优解。
- 多阶段决策: 意味着问题可以分解成子问题。也就是说问题可以拆分成多个子问题进行求解,并且子问题之间还有重叠的更小的子问题。
- 最优子结构: 在自下而上的递推过程中,我们求得的每个子问题一定是全局最优解,既然它分解的子问题是全局最优解,那么依赖于它们解的原问题自然也是全局最优解。
- 自下而上: 怎样才能自下而上的求出每个子问题的最优解呢,可以肯定子问题之间是有一定联系的,即迭代递推公式,也叫 「状态转移方程」,要定义好这个状态转移方程, 我们就需要定义好每个子问题的状态(DP 状态),那为啥要自下而上地求解呢,因为如果采用像递归这样自顶向下的求解方式,子问题之间可能存在大量的重叠,大量地重叠子问题意味着大量地重复计算,这样时间复杂度很可能呈指数级上升(在下文中我们会看到多个这样重复的计算导致的指数级的时间复杂度),所以自下而上的求解方式可以消除重叠子问题。
最优子结构,状态转移方程,重叠子问题 就是动态规划的三要素,这其中定义子问题的状态与写出状态转移方程是解决动态规划最为关键的步骤,状态转移方程如果定义好了,解决动态规划就基本不是问题了。
2、实例分析
2.1 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
解法一:动态规划
对于一个长度大于2的子串如何能确定其是否为回文串呢?对于 " a b c b a " "abcba" "abcba"这样一个回文串,可以看出边界上的两个字符一定是相等的,去掉边界之后的子串也必须是回文串。我们设 s [ i , j ] s[i,j] s[i,j]为始于 i i i 终于 j j j 的一个子串(包含 s [ i ] s[i] s[i] 和 s [ j ] s[j] s[j]), P ( i , j ) P(i, j) P(i,j)为该子串是否为回文串的flag。所以有:
P ( i , j ) = { true, 如果子串 S i … S j 是回文中 false, 其它情况 P(i, j)=\left\{\begin{array}{ll}\text { true, } & \text { 如果子串 } S_{i} \ldots S_{j} \text { 是回文中 } \\ \text { false, } & \text { 其它情况 }\end{array}\right. P(i,j)={ true, false, 如果子串 Si…Sj 是回文中 其它情况
P ( i , j ) P(i, j) P(i,j)是否为回文串有两个条件:
- P ( i + 1 , j − 1 ) P(i+1, j-1) P(i+1,j−1)为回文串
- s [ i ] = = s [ j ] s[i] == s[j] s[i]==s[j]
P ( i , j ) = P ( i + 1 , j − 1 ) ∧ ( S i = = S j ) P(i, j)=P(i+1, j-1) \wedge\left(S_{i}==S_{j}\right) P(i,j)=P(i+1,j−1)∧(Si==Sj)
当子串长度小于等于2时,可以得到边界条件:
{ P ( i , i ) = true P ( i , i + 1 ) = ( S i = = S i + 1 ) \left\{\begin{array}{l} P(i, i)=\text { true } \\ P(i, i+1)=\left(S_{i}==S_{i+1}\right) \end{array}\right. {P(i,i)= true P(i,i+1)=(Si==Si+1)
根据这个思路,我们就可以完成动态规划了。
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
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;
}
else if(l == 1)
{
dp[i][j] = (s[i] ==s[j])?1:0;
}
else
{
dp[i][j] = (dp[i+1][j-1] && s[i] == s[j]);
}
if(ans.length()<l+1 && dp[i][j])
{
ans = s.substr(i, l+1);
}
}
}
return ans;
}
};
但动态规划的成绩不是很理想。
解法二:中心扩展算法:
class Solution {
public:
pair<int, int> expendAroundCenter(string &s, int left, int right)
{
while(left>=0&&right<s.length()&&s[left]==s[right])
{
left--;
right++;
}
return {left+1, right-1};
}
string longestPalindrome(string s) {
int n = s.length();
int start = 0, end = 0;
for(int i=0; i<n; i++)
{
auto [left1, right1] = expendAroundCenter(s, i, i);
auto [left2, right2] = expendAroundCenter(s, i, i+1);
if(right1 - left1 > end - start)
{
start = left1;
end = right1;
}
if(right2 - left2 > end - start)
{
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
};
有兴趣的可以对比一下两种算法所消耗的时间,对于细节就不做讲解了,欢迎在评论区提问。