今天继续动态规划~
5.最长回文子串
方法一:动态规划
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串。例如对于字符串 “ababa”,如果我们已经知道 “bab” 是回文串,那么 “ababa” 一定是回文串,这是因为它的首尾两个字母都是 “a”。
根据这样的思路,我们就可以用动态规划的方法解决本题。我们用 P(i,j) 表示字符串 s 的第 i 到 j 个字母组成的串(下文表示成 s[i:j])是否为回文串:
这里的「其它情况」包含两种可能性:
那么我们就可以写出动态规划的状态转移方程:
也就是说,只有 s[i+1:j−1] 是回文串,并且 s 的第 i 和 j 个字母相同时,s[i:j] 才会是回文串。
上文的所有讨论是建立在子串长度大于 2 的前提之上的,我们还需要考虑动态规划中的边界条件,即子串的长度为 1 或 2。对于长度为 1 的子串,它显然是个回文串;对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。因此我们就可以写出动态规划的边界条件:
根据这个思路,我们就可以完成动态规划了,最终的答案即为所有 P(i,j)=true 中 j−i+1(即子串长度)的最大值。注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
//DP
class Solution {
public:
string longestPalindrome(string s) {
int n = s.size();
vector<vector<int>> dp(n,vector<int>(n,false));
int result = 1;
string str;
//反向遍历
for(int i=n-1;i>=0;i--){
for(int j=i;j<s.size();j++){
if(s[i]==s[j] && (j-i<=1 || dp[i+1][j-1])){
dp[i][j] = true;
result = max(result,j-i+1);
if(j-i+1>=result){
str = s.substr(i,j-i+1);
}
}
}
}
return str;
}
};
516.最长回文子序列
对于一个子序列而言,如果它是回文子序列,并且长度大于 2,那么将它首尾的两个字符去除之后,它仍然是个回文子序列。因此可以用动态规划的方法计算给定字符串的最长回文子序列。
用 dp[i][j] 表示字符串 s 的下标范围 [i,j] 内的最长回文子序列的长度。假设字符串 s 的长度为 n,则只有当 0≤i≤j<n 时,才会有 dp[i][j]>0,否则 dp[i][j]=0。
由于任何长度为 1 的子序列都是回文子序列,因此动态规划的边界情况是,对任意 0≤i<n,都有 dp[i][i]=1。
当 i<j 时,计算 dp[i][j] 需要分别考虑 s[i] 和 s[j] 相等和不相等的情况:
1、如果 s[i]=s[j],则首先得到 s 的下标范围 [i+1,j−1] 内的最长回文子序列,然后在该子序列的首尾分别添加 s[i] 和 s[j],即可得到 s 的下标范围 [i,j] 内的最长回文子序列,因此
dp[i][j] = dp[i+1][j−1] + 2;
如果 s[i]!=s[j],则 s[i] 和 s[j] 不可能同时作为同一个回文子序列的首尾,因此
dp[i][j] = max( dp[i+1][j] , dp[i][j−1] )。
由于状态转移方程都是从长度较短的子序列向长度较长的子序列转移,因此需要注意动态规划的循环顺序。
最终得到 dp[0][n−1] 即为字符串 s 的最长回文子序列的长度。
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n,vector<int>(n));
for(int i=n-1;i>=0;i--){
dp[i][i] = 1;
char c1 = s[i];
for(int j=i+1;j<n;j++){
char c2 = s[j];
if(c1 == c2){
dp[i][j] = dp[i+1][j-1] + 2;
}
else{
dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
};
42.接雨水
方法一:动态规划
对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 i 处能接的雨水量等于下标 i 处的水能到达的最大高度减去 height[i]。
创建两个长度为 n 的数组 leftMax 和 rightMax。对于 0≤i<n,leftMax[i] 表示下标 i 及其左边的位置中,height 的最大高度,rightMax[i] 表示下标 i 及其右边的位置中,height 的最大高度。
显然,leftMax[0]=height[0],rightMax[n−1]=height[n−1]。两个数组的其余元素的计算如下:
因此可以正向遍历数组 height 得到数组 leftMax 的每个元素值,反向遍历数组 height 得到数组 rightMax 的每个元素值。
在得到数组 leftMax 和 rightMax 的每个元素值之后,对于 0≤i<n,下标 i 处能接的雨水量等于 min(leftMax[i],rightMax[i])−height[i]。遍历每个下标位置即可得到能接的雨水总量。
动态规划做法可以由下图体现。
官方题解链接:https://leetcode.cn/problems/trapping-rain-water/solutions/692342/jie-yu-shui-by-leetcode-solution-tuvc/
class Solution{
public:
int trap(vector<int>& height){
int n = height.size();
if(n==0){
return 0;
}
vector<int> leftMax(n);
leftMax[0] = height[0];
for(int i=1;i<n;i++){
leftMax[i] = max(height[i],leftMax[i-1]);
}
vector<int> rightMax(n);
rightMax[n-1] = height[n-1];
for(int i=n-2;i>=0;i--){
rightMax[i] = max(height[i],rightMax[i+1]);
}
int ans = 0;
for(int i=0;i<n;i++){
ans += min(leftMax[i],rightMax[i]) - height[i];
}
return ans;
}
};
//还有单调栈、双指针等方法,我且弄完动态规划之后再来。