目录
32. 最长有效括号
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:
输入:s = ""
输出:0
思路
首先,我们定义一个dp数组,表示以第i个元素结尾的字符串的最长有效括号的长度。
对于dp[i]
来说,它可能有以下两种情况:
s[i] == '('
:这时他是无法和前面的括号组成有效括号对的,所以dp[i] = 0
。s[i] == ')'
:这时我们需要确定它是否能和前面的元素组成有效括号对,那么还需要去观察s[i-1]
:s[i-1] == '('
:s[i-1]
刚好和s[i]
组成一对有效括号,长度为2。那么这种情况下我们可以推导出: d p [ i ] = d p [ i − 2 ] + 2 dp[i] = dp[i-2] + 2 dp[i]=dp[i−2]+2s[i-1] == ')'
:对于这种情况来说,我们不知道s[i-1]
是否和前面的元素组成有效括号,但无论如何,dp[i-1]
中存放的总是以s[i-1]
结尾的字符串的最长有效括号长度。假设s[i-1]
是有效的括号对之一,那么与他配对的括号index就是i-dp[i-1]
,于是乎我们就可以找到和s[i]
配对的位置i-dp[i-1]-1
,这样如果s[i-dp[i-1]-1] == '('
,那么s[i]
就能与他配对上。那么我们就可以推导出: d p [ i ] = d p [ i − 1 ] + 2 + d p [ i − d p [ i − 1 ] − 2 ] dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2] dp[i]=dp[i−1]+2+dp[i−dp[i−1]−2]
注意,上面要需要加上
d
p
[
i
−
d
p
[
i
−
1
]
−
2
]
dp[i-dp[i-1]-2]
dp[i−dp[i−1]−2],这是因为如果我们判定s[i-dp[i-1]-1] == '('
与s[i]
配对的话,还需要考虑s[i-dp[i-1]-1]
之前的情况。
代码
class Solution {
public:
int longestValidParentheses(string s) {
vector<int> dp(s.size(), 0);
int ans = 0;
for(int i = 1; i < s.size(); ++i){
if(s[i] == ')'){
// 前面一个是(的情况,直接配对成功
if(s[i - 1] == '('){
dp[i] = 2;
// 加上前面配对的数量
if(i - 2 >= 0) dp[i] += dp[i - 2];
}
// 前面一个也是),但是有配对的
else if(dp[i - 1] > 0){
// 判断匹配位置的符号是不是(,如果是则可以配对
if((i - dp[i - 1] - 1) >= 0 && s[i - dp[i - 1] - 1] == '('){
dp[i] = dp[i - 1] + 2;
// 加上前面配对的数量
if(i - dp[i - 1] - 2 >= 0)
dp[i] += dp[i - dp[i - 1] - 2];
}
}
}
ans = max(ans, dp[i]);
}
return ans;
}
};
300. 最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101]
,因此长度为 4
。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
思路
dp[i]
表示第i个数为结尾的子序列的最长严格递增长度。
很容易的就能想到,我们要计算的dp[i]
一共有两种情况:
- nums[i] > nums[i - 1]
:此时dp[i] = dp[i-1] + 1
- nums[i] < nums[i - 1]
:此时dp[i] = dp[i-1]
但是如果我们简单的将代码写成这样:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1);
for(int i = 1; i < nums.size(); ++i){
if(nums[i] > nums[i - 1])
dp[i] = dp[i - 1] + 1;
else
dp[i] = dp[i-1];
}
return dp[nums.size()-1];
}
当测试用例为:nums = [4,10,4,3,8,9]
。我们打印出上面代码计算出的dp数组:1 2 2 2 3 4
。可以发现并不是我们所期望的那样,当i=4
时,dp[4]
应该为2
,而不是3
。
我们计算成3
是因为:对于子数组[4,10,4,3]
来说,它的最大递增子序列为4, 10
。我们如果只是简单的判断nums[4] > nums[3]
,就执行dp[i] = dp[i - 1] + 1
,那么就相当于是把4, 10, 8
当成了最大递增子序列,然而它并不是的,所以这里就出现了判断失误。
正确的做法是,我们要把dp[i]
定义为第i
个数为结尾的子数组的最长严格递增长度,并且该最长严格递增子序列最后一个数必定是nums[i]
。
每次进行判断的时候,我们要对该子数组各个位置nums[j]
进行遍历,并比较与nums[i]
的大小,如果nums[i] > nums[j]
,那么就有dp[i] = max(dp[i], dp[j] + 1)
。
最终答案就是dp数组中最大的那个数。
代码
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1);
int ans = 1;
for(int i = 1; i < nums.size(); ++i){
for(int j = 0; j < i; ++j){
if(nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
}
ans = max(ans, dp[i]);
}
return ans;
}
};
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5]
, 长度为3
。
尽管 [1,3,5,7]
也是升序的子序列, 但它不是连续的,因为 5
和 7
在原数组里被 4
隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2]
, 长度为1
。
思路1:双指针
定义两个指针:int slow
和int fast
。
代码1:双指针
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int slow = 0;
int ans = 0;
while(slow < nums.size()){
int fast = slow + 1;
while(fast < nums.size() && nums[fast] > nums[fast - 1]){
fast++;
}
ans = max(ans, fast - slow);
slow = fast;
}
return ans;
}
};
思路2:dp
dp的思路和上题类似。我们定义一个dp数组,dp[i]
表示第i个数为结尾的子数组的最长连续递增长度,并且该最长连续递增序列最后一个数必定是nums[i]
。
与上题不一样的就是我们不在需要遍历子数组了,直接判断nums[i]
和nums[i - 1]
的大小即可。
代码2:dp
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1);
int ans = 1;
for(int i = 1; i < nums.size(); ++i){
if(nums[i] > nums[i - 1]){
dp[i] = max(dp[i], dp[i - 1] + 1);
}
ans = max(ans, dp[i]);
}
return ans;
}
};
718. 最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1]
, nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1]
。
示例 2:
输入:nums1 = [0,0,0,0,0]
, nums2 = [0,0,0,0,0]
输出:5
思路1:dp
dp[i][j]
表示数组nums1
前i
个元素和数组nums2
前j
个元素的最长公共子数组的长度。其实也是和上题一样的。
代码1:dp
我们可以很快写出代码:
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size(), vector<int>(nums2.size(), 0));
int ans = 0;
// dp数组初始化
for(int i = 0; i < nums1.size(); ++i){
if(nums2[0] == nums1[i]) dp[i][0] = 1;
if(dp[i][0] > ans) ans = dp[i][0];
}
for(int j = 0; j < nums2.size(); ++j){
if(nums1[0] == nums2[j]) dp[0][j] = 1;
if(dp[0][j] > ans) ans = dp[0][j];
}
// dp数组推导
for(int i = 1; i < nums1.size(); ++i){
for(int j = 1; j < nums2.size(); ++j){
if(nums1[i] == nums2[j])
dp[i][j] = dp[i - 1][j - 1] + 1;
if(dp[i][j] > ans)
ans = dp[i][j];
}
}
return ans;
}
};
注意初始化的时候我们也要去更新ans的大小,不然会漏结果。
思路2:dp优化
其实,我们可以将dp[i][j]
定义为数组nums1
前i-1
个元素和数组nums2
前j-1
个元素的最长公共子数组的长度,也就是在nums1前增加一行,nums2前增加一列:
这样我们在定义数组的时候就已经初始化好了,不必额外去for循环初始化:vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
代码2:dp优化
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int ans = 0;
// dp数组推导
for(int i = 1; i <= nums1.size(); ++i){
for(int j = 1; j <= nums2.size(); ++j){
if(nums1[i - 1] == nums2[j - 1])
dp[i][j] = dp[i - 1][j - 1] + 1;
if(dp[i][j] > ans)
ans = dp[i][j];
}
}
return ans;
}
};
1143. 最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace"
是 "abcde"
的子序列,但 "aec"
不是 "abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace"
,它的长度为 3
。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc"
,它的长度为 3
。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0
。
思路
这题和上题的不同之处在于 公共子序列和原字符串是相对有序的。感觉这题有点像300. 最长递增子序列和718. 最长重复子数组的结合体。
上一题以i-1
和j-1
是因为子数组必须要求是连续的,如果不连续,公共子数组直接归零,下一个子数组不能继承前一个子数组的公共子数组长度。
子序列则不一样,允许中间有间隔,下一个子序列可以继承前一个子序列的公共子序列长度。
这样说很抽象,我们举个例子。比如说两个数组nums1 = [1,2,3,4,5]
, nums2 = [1,2,3,8,5]
。在index=3
的时候出现分歧了,如果是公共子数组,到index=3
时,其公共子数组必须要归零,如果不归零,会影响index=4
的判断。而如果是公共子序列,index=3
可以保留index=2
的最长子序列数,继而在index=4
时继续递增。
代码
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int len1 = text1.size();
int len2 = text2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for(int i = 1; i <= len1; ++i){
for(int j = 1; j <= len2; ++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[len1][len2];
}
};
1035. 不相交的线(最大连线数)
在两条独立的水平线上按给定的顺序写下 nums1
和 nums2
中的整数。
现在,可以绘制一些连接两个数字 nums1[i]
和 nums2[j]
的直线,这些直线需要同时满足:
nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 1:
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 但无法画出第三条不相交的直线,因为从 nums1[1]=4
到 nums2[2]=4
的直线将与从 nums1[2]=2
到 nums2[1]=2
的直线相交。
示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3
示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2
思路
和1143. 最长公共子序列一模一样。
代码
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int len2 = nums2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for(int i = 1; i <= len1; ++i){
for(int j = 1; j <= len2; ++j){
if(nums1[i - 1] == nums2[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[len1][len2];
}
};
53. 最大子序和
给你一个整数数组 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:贪心
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int count = 0;
for(int i = 0; i < nums.size(); i++) {
count += nums[i];
if(count > result) result = count;
if(count <= 0) count = 0;
}
return result;
}
};
思路2:dp
dp[i]
表示以nums[i-1]
为结尾的最大子数组的和为dp[i]
。
对于每个dp[i]
都有两种情况:
nums[i-1]
加入到前面的子数组中,也就是dp[i] = dp[i - 1] + nums[i - 1]
nums[i-1]
不加入到前面的子数组中,从它这里重新开始计算和,也就是dp[i] = numd[i - 1]
- 我们取最大的值:
dp[i] = max(nums[i - 1], dp[i - 1] + nums[i - 1])
代码2:dp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size() + 1, 0);
int ans = INT_MIN;
for(int i = 1; i <= nums.size(); ++i){
dp[i] = max(nums[i - 1], dp[i - 1] + nums[i - 1]);
if(dp[i] > ans) ans = dp[i];
}
return ans;
}
};
516. 最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb"
。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb"
。
思路
- dp数组含义:
dp[i][j]
表示从i
到j
中的最长回文子序列的长度。 - 推导公式:有两种情况
s[i] == s[j]
:那么就可以把这一对元素加入到最长回文子序列中,dp[i][j] = dp[i+1][j-1] + 2
s[i] != s[j]
:因为他们不能凑成一对,所以我们需要另外去考虑单独加上他们任意一个到中间的序列中是否会凑成回文,dp[i][j]=max(dp[i+1][j], dp[i][j-1])
- dp初始化:当
i==j
时,就是一个元素,也是能组成回文的,所以dp[i][j]=1
- 推导遍历顺序:从下图可以看出是从下到上,从左到右
代码
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
// 初始化
for(int i = 0; i < s.size(); ++i){
dp[i][i] = 1;
}
// 推导
for(int i = s.size() - 1; i >= 0; --i){
for(int j = i + 1; j < s.size(); ++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][s.size()-1];
}
};
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的 回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba"
同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
思路
这题和上题不同的地方在于,也就是序列和连续串的区别。
- dp数组含义:
dp[i][j]
表示从i
到j
中的子串如果是回文串,dp[i][j] = 1
,如果不是,dp[i][j] = 0
。 - 推导公式:有两种情况
s[i] == s[j] && dp[i+1][j-1] == 1
:也就是说两端元素相等,且中间子串也为回文,那么dp[i][j] = 1
s[i] != s[j]
:他们不能凑成回文,所以他们的dp[i][j] = 0
,不用管。
- dp初始化:
- 当
i==j
时,就是一个元素,也是能组成回文的,所以dp[i][j]=1
- 如果前后两个元素相等,
s[i] == s[i+1]
,那么也是回文,dp[i][i+1]=1
- 当
- 推导遍历顺序:因为能组成的回文串长度只可能是从1到
s.size()
,而在前面初始化的时候我们已经判断了长度为1和2的回文子串,那么在遍历的时候我们从3开始即可。从左往右我们依次判断长度为l
的子串头尾是否相等,且中间子串是否为回文(dp[i][j] == 1
),这样去记录能组成回文串的长度以及左边界。因为我们是按照子串从短到长进行遍历的,所以最后记录的回文串长度一定是最长的。
代码
class Solution {
public:
string longestPalindrome(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
if(s.size() == 1) return s;
int left = 0;
int max_len = 1;
// 初始化
for(int i = 0; i < s.size(); ++i){
dp[i][i] = 1;
if(i < s.size()-1 && s[i] == s[i+1]){
dp[i][i+1] = 1;
max_len = 2;
left = i;
}
}
// 推导
for(int l = 3; l <= s.size(); ++l){ // 枚举子串长度
for(int i = 0; i + l - 1 < s.size(); ++i){ // 枚举左边界
int j = i + l - 1; // 右边界
if(s[i] == s[j] && dp[i+1][j-1]){
dp[i][j] = 1;
max_len = l;
left = i;
}
}
}
return s.substr(left, max_len);
}
};