本文参考作者:labuladong
两种思路
1、第一种思路模板是一个一维的 dp 数组:
int n = array.length;
int[] dp = new int[n];
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
dp[i] = 最值(dp[i], dp[j] + ...)
}
}
例如「最长递增子序列」,在这个思路中 dp 数组的定义是:
在子数组 array[0…i] 中,我们要求的子序列(最长递增子序列)的长度是 dp[i]。
2、第二种思路模板是一个二维的 dp 数组:
int n = arr.length;
int[][] dp = new dp[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i] == arr[j])
dp[i][j] = dp[i][j] + ...
else
dp[i][j] = 最值(...)
}
}
这种思路运用相对更多一些,尤其是涉及两个字符串/数组的子序列,比如「最长公共子序列」。本思路中 dp 数组含义又分为「只涉及一个字符串」和「涉及两个字符串」两种情况。
2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 数组的含义如下:
在子数组 arr1[0…i] 和子数组 arr2[0…j] 中,我们要求的子序列(最长公共子序列)长度为 dp[i][j]。
2.2 只涉及一个字符串/数组时(比如本文要讲的最长回文子序列),dp 数组的含义如下:
在子数组 array[i…j] 中,我们要求的子序列(最长回文子序列)的长度为 dp[i][j]。
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
//动态规划
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> F(nums.size(),1);//F[i]表示以nums[i]结尾的最长公共子序列的长度
for(int i=1,j;i<nums.size();i++){
for(j=i-1;j>=0;j--){
if(nums[j]<nums[i]&&F[i]<F[j]+1){
F[i]=F[j]+1;
}
}
}
int max=0;
for(int i=0;i<F.size();i++){
if(F[i]>max){
max=F[i];
}
}
return max;
}
};
时间复杂度:O(N^2),这里 N 是数组的长度,我们写了两个 for 循环,每个 for 循环的时间复杂度都是线性的。
空间复杂度:O(N),要使用和输入数组长度相等的状态数组,因此空间复杂度是 O(N)。
链接
来源:力扣(LeetCode)
673. 最长递增子序列的个数
给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数。
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
vector<int> count(nums.size(),1);
vector<int> num_count(nums.size(),1);
//count[i]表示以nums[i]为结尾的最长递增子序列的长度
//num_count表示以nums[i]为结尾的最长递增子序列的个数(注:不是最长子序列的个数)
for(int i=1;i<nums.size();i++){
for(int j=i-1;j>=0;j--){
if(nums[i]>nums[j]&&count[i]<count[j]+1){
count[i]=count[j]+1;
num_count[i]=num_count[j];//发现更长的子序列
}
else if(nums[i]>nums[j]&&count[i]==count[j]+1){
num_count[i]+=num_count[j];//发现长度相同的子序列
}
}
}
int loc_max=0,ret=0;
for(int i=0;i<count.size();i++){
if(count[loc_max]<count[i]){
loc_max=i;
}
}
for(int i=0;i<count.size();i++){
if(count[loc_max]==count[i]){
ret+=num_count[i];
}
}
//return num_count[loc_max];//错误
return ret;
}
};
来源:力扣(LeetCode)
链接
516. 最长回文子序列
给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入:
"bbbab"
输出:
4
一个可能的最长回文子序列为 "bbbb"。
示例 2:
输入:
"cbbd"
输出:
2
一个可能的最长回文子序列为 "bb"。
提示:
- 1 <= s.length <= 1000
- s 只包含小写英文字母
class Solution {
public:
int longestPalindromeSubseq(string s) {
//区间规划
int n=s.size();
vector<vector<int>> dp(n,vector<int>(n,0));
//dp[i][j]表示s[i]~s[j]之间的最长回文子序列
for(int i=0;i<n;i++){
dp[i][i]=1;
}
//为了保证每次计算 dp[i][j],左下右方向的位置已经被计算出来,只能斜着遍历或者反着遍历:
//反着遍历保证正确的状态转移
for(int i=n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
//状态转移方程
if(s[i]==s[j]){
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];
}
};
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m=text1.size(),n=text2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
//dp[i][j]表示text1[0~i-1]和text2[0~j-1]的最长公共子序列
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(text1[i-1]==text2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
5. 最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:
输入: “cbbd”
输出: “bb”
//法一:动态规划法
class Solution {
public:
string longestPalindrome(string s) {
string ans;
int N=s.size();
vector<vector<bool>> dp(N,vector<bool>(N,false));
//dp[i][j]=true表示si...sj为回文子串
//从长度较短的字符串向长度较长的字符串转移
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]=true;
}
else if(l==1){
dp[i][j]=(s[i]==s[j]);
}
else{
dp[i][j]=dp[i+1][j-1]&&(s[i]==s[j]);
}
if(dp[i][j]&&l+1>ans.size()){
ans=s.substr(i,l+1);
}
}
}
return ans;
}
};
//法二:中心扩展算法
/*枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。*/
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {left + 1, right - 1};
}
string longestPalindrome(string s) {
int start = 0, end = 0;
for (int i = 0; i < s.size(); ++i) {
auto [left1, right1] = expandAroundCenter(s, i, i);
auto [left2, right2] = expandAroundCenter(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);
}
};