解题思想
子数组问题的解法思路如下:
- 不连续子数组:动态规划
- 连续子数组:累加或者累乘
leetcode 不连续子数组
[hot] 39. 组合总和
题目
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。
说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
题解
本地重点在于数组元素可以无限次重复,如果不重复可以通过动态规划解决,而现在只能通过递归法解决该问题。解析思路如下所示(盗取leetcode官方图):
示例代码如下所示:
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
dfs(candidates, target, 0);
return res;
}
void dfs(vector<int>& cands, int target, int idx) {
if (idx == cands.size()) {
return;
}
if (target == 0) {
res.emplace_back(com);
return;
}
// 直接跳过
dfs(cands, target, idx + 1);
if (target >= cands[idx]) {
com.emplace_back(cands[idx]);
dfs(cands, target - cands[idx], idx);
com.pop_back();
}
}
private:
vector<vector<int>> res;
vector<int> com;
};
复杂度
时间复杂度:所有节点的深度之和
空间复杂度:
O
(
t
a
r
g
e
t
)
O(target)
O(target),最坏情况下target每次在递归的时候只减1,这时递归树变为一条双向链表
关键点
理解数组元素能够重复利用的原理
40. 组合总和 II
题目
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
题解
回溯法解决问题,需要注意如下两个问题,而且需要注意本题和【39. 组合总和】的区别,数组每个元素只能push到目标数组一次。这道题算是<1. 判断子数组的任意子集的和是否有可能组成target>的进化版,需要找到所有组合,但后者只需要判断是否存在。示例代码如下所示:
class Solution {
public:
void dfs(vector<int>& candidates, int target, int start) {
if (target == 0) {
result.emplace_back(path);
return;
}
int length = candidates.size();
for (int i = start; i < length && target - candidates[i] >= 0; ++i) {
// 判断新加入的元素是否等于上次回退的元素
/*
** 这里的原理在于下面的dfs()idx的参数是i+1,当上一次递归返回后,
** 因为经过了排序,调用者的idx可能指向与回退的元素相同的值,造成结果的重复,如:
** 在例子[1,1,2,5,6,7,10],target=8的例子(假设已经sort过了)中,
** 当idx = 0时,已经配对了[1,2,5]的结果,当最后回溯到该调用(即第一个调用)后,
** 迭代到idx = 1时,该位置上的值还是1,且因为升序排序,此时若不讲idx=1排除,
** 则它仍可以与后续的元素再匹配出一个[1,2,5]的结果。
*/
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
path.emplace_back(candidates[i]);
dfs(candidates, target - candidates[i], i + 1);
// 还原
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if (candidates.empty()) {
return result;
}
sort(candidates.begin(), candidates.end());
dfs(candidates, target, 0);
return result;
}
private:
vector<vector<int>> result;
vector<int> path;
};
复杂度
时间复杂度:
O
(
2
n
∗
n
)
O(2^n * n)
O(2n∗n),回溯法较为宽松的上界
空间复杂度:
O
(
n
)
O(n)
O(n)
关键点
理解重复元素跳过递归的操作
[hot] 46. 全排列
题目
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
题解
回溯法解决问题,整体题解可见这里,按照题解画出递归树,并对递归树进行注释,示意图如下所示:
示例代码如下所示:
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
dfs(0, nums);
return res;
}
void dfs(int first, vector<int>& nums) {
if (first == nums.size()) {
res.emplace_back(nums);
return;
}
for (int i = first; i < nums.size(); ++i) {
swap(nums[i], nums[first]);
dfs(first + 1, nums);
swap(nums[i], nums[first]);
}
}
private:
vector<vector<int>> res;
};
复杂度
时间复杂度:站在第一个元素的角度,其本身有n种可能取值,第二位有n-1种取值,…因而对于第一个元素有n!的时间复杂度,而对于每一个元素来说时间复杂度相同,因而总的时间复杂度为
O
(
n
∗
n
!
)
O(n*n!)
O(n∗n!)
空间复杂度:
O
(
n
)
O(n)
O(n),递归调用栈最多递归n次。
关键点
理解回溯树
[hot] 76. 最小覆盖子串
题目
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
题解
我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的 rrr 指针,和一个用于「收缩」窗口的 lll 指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 sss 上滑动窗口,通过移动 rrr 指针不断扩张窗口。当窗口包含 ttt 全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
示例代码如下所示:
class Solution {
public:
bool check() {
for (const auto& p: t_ctnr) {
if (s_ctnr[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto& c: t) {
++t_ctnr[c];
}
int ans_l = -1;
int res_len = INT_MAX;
int l = 0, r = 0;
while (r < int(s.size())) {
if (t_ctnr.count(s[r]) > 0) {
++s_ctnr[s[r]];
}
while (l <= r && check()) {
if (r + 1 - l < res_len) {
res_len = r + 1 - l;
ans_l = l;
}
if (t_ctnr.count(s[l]) > 0) {
--s_ctnr[s[l]];
}
++l;
}
++r;
}
return ans_l == -1 ? "" : s.substr(ans_l, res_len);
}
private:
unordered_map<char, int> t_ctnr, s_ctnr;
};
复杂度
时间复杂度:设字符集大小为C,s的遍历次数最多为s.size(),每次check()最高时间复杂度为
C
C
C,总体时间复杂度为
O
(
C
∗
∣
s
∣
+
∣
t
∣
)
O(C * |s| + |t|)
O(C∗∣s∣+∣t∣)
空间复杂度:
O
(
C
)
O(C)
O(C)
关键点
滑动窗口开始滑动时,更新结果
[hot] 78. 子集
题目
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
题解
回溯法从前到后遍历数组,递归树示意图如下所示:
示例代码如下所示:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
bt(0, nums);
return res;
}
void bt(int idx, vector<int>& nums) {
res.emplace_back(path);
for (int i = idx; i < nums.size(); ++i) {
path.emplace_back(nums[i]);
bt(i + 1, nums);
path.pop_back();
}
}
private:
vector<vector<int>> res;
vector<int> path;
};
这里总结了较为精辟的回溯法流程:
复杂度
时间复杂度:
O
(
n
∗
2
n
)
O(n * 2^n)
O(n∗2n),
O
(
n
)
O(n)
O(n)时间递归,总共有
2
n
2^n
2n中可能性
空间复杂度:
O
(
n
)
O(n)
O(n),递归调用栈最长为n
90. 子集 II
题目
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
题解
在【78. 子集】的基础上,需要引入两个变量:
- 数组正序排序 -> 重复元素相邻,这样好剪枝。
- 剪枝 -> 剪枝条件:递归树的同层节点中,重复节点可以直接跳过。
递归树示意图如下所示,其中红框是被剪枝掉的递归过程。
示例代码如下所示:
class Solution {
public:
void bt(vector<int>& nums, int startIndex) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
// i > startIndex -> 只需要在同层判断重复,跨层不用管
if (i > startIndex && nums[i] == nums[i - 1]) {
continue;
}
path.push_back(nums[i]);
bt(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
bt(nums, 0);
return result;
}
private:
vector<vector<int>> result;
vector<int> path;
};
复杂度
时间复杂度:
O
(
n
∗
2
n
)
O(n * 2^n)
O(n∗2n),
O
(
n
)
O(n)
O(n)时间递归,总共有
2
n
2^n
2n中可能性
空间复杂度:
O
(
n
)
O(n)
O(n),递归调用栈最长为n
[hot] 128. 最长连续序列
题目
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
进阶:你可以设计并实现时间复杂度为 O(n) 的解决方案吗?
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
题解
利用哈希表储存每个元素已达到去重的目的,进而找到最终结果,示例代码如下所示。
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int length = nums.size();
if (length == 0) {
return 0;
}
unordered_set<int> s;
for (auto n: nums) {
s.insert(n);
}
int res = 0;
for (auto n : nums) {
if (s.count(n - 1) == 0) {
int cur_n = n;
int cur_len = 0;
while (s.count(cur_n)) {
++cur_n;
++cur_len;
}
res = max(cur_len, res);
}
}
return res;
}
};
复杂度
时间复杂度:一个元素如果在m中遍历过,则再也不会被重复遍历,因而时间复杂度为
O
(
n
)
O(n)
O(n),虽然嵌套了两层循环。
空间复杂度:m引入额外的存储空间,因而空间复杂度为
O
(
n
)
O(n)
O(n)
题解2
哈希表的元素记录左右端点的位置,示例代码如下所示:
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int curlen = 0;
int maxlen = 0;
int left = 0, right = 0;
unordered_map<int, int> map;
for (auto num : nums) {
if (map[num]) {
continue;
}
// 如果num没有在map中出现,则left一定是num-1为右端点的子序列长度
// right一定是num+1为左端点的子序列长度
left = map[num - 1];
right = map[num + 1];
curlen = left + 1 + right;
maxlen = max(maxlen, curlen);
map[num] = curlen;
map[num - left] = curlen;
map[num + right] = curlen;
}
return maxlen;
}
};
[hot] 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数组,dp[i]表示的是以nums[i]为结尾的升序子序列的最大长度,转移方程如下所示:
d
p
[
i
]
=
m
a
x
(
d
p
[
j
]
+
1
,
d
p
[
i
]
)
0
≤
j
<
i
dp[i]= max(dp[j] + 1, dp[i]) \ \ 0\leq j<i
dp[i]=max(dp[j]+1,dp[i]) 0≤j<i
示例代码如下所示:
// 一维dp数组
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int length = nums.size();
vector<int> dp(length, 1);
int res = 1;
for (int i = 1; i < length; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
res = max(dp[i], res);
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),因为要遍历数组
n
2
n^2
n2次
空间复杂度:
O
(
n
)
O(n)
O(n),动态规划数组的存储空间
题解2
动态规划 + 二分法,动态规划方程组d[i]
代表长度为i的一系列严格递增子序列中,末尾元素最小的那个子序列对应的末尾元素,需要维护该数组为严格递增的,因而可以通过二分法的形式,示例代码如下所示:
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
// len初始化为1
int len = 1, n = (int)nums.size();
if (n == 0) {
return 0;
}
vector<int> d(n + 1, 0);
d[len] = nums[0];
for (int i = 1; i < n; ++i) {
if (nums[i] > d[len]) {
d[++len] = nums[i];
} else {
// 二分法插入元素可操作范围为[1, len],没有0
// 需要深刻理解动态规划方程组的含义
int l = 1;
int r = len;
int target = nums[i];
while (l <= r) {
int mid = (l + r) / 2;
if (target > d[mid]) {
l = mid + 1;
} else {
r = mid - 1;
}
}
d[l] = target;
}
}
return len;
}
};
复杂度
时间:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),二分法+ 一遍遍历的时间复杂度
空间:
O
(
n
)
O(n)
O(n)
关键点
深刻理解动态规划方程组的含义
392. 判断子序列
题目
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
输入:s = "abc", t = "ahbgdc"
输出:true
示例 2:
输入:s = "axc", t = "ahbgdc"
输出:false
题解
正常遍历两个数组完成题目,示例代码如下所示:
class Solution {
public:
bool isSubsequence(string s, string t) {
int m = s.size();
int n = t.size();
int i = 0;
int j = 0;
while (i < m && j < n) {
if (s[i] == t[j]) {
++i;
}
++j;
}
return i == m;
}
};
复杂度
时间复杂度:
O
(
m
a
x
(
m
,
n
)
)
O(max(m, n))
O(max(m,n))
空间空间:
O
(
1
)
O(1)
O(1)
关键点
不连续匹配
题解2
动态规划解决问题,题解见 添加链接描述,示例代码如下所示:
class Solution {
public:
bool isSubsequence(string s, string t) {
int n = s.size(), m = t.size();
vector<vector<int> > f(m + 1, vector<int>(26, 0));
for (int i = 0; i < 26; i++) {
f[m][i] = m;
}
for (int i = m - 1; i >= 0; i--) {
for (int j = 0; j < 26; j++) {
if (t[i] == j + 'a')
f[i][j] = i;
else
f[i][j] = f[i + 1][j];
}
}
int add = 0;
for (int i = 0; i < n; i++) {
if (f[add][s[i] - 'a'] == m) {
return false;
}
add = f[add][s[i] - 'a'] + 1;
}
return true;
}
};
复杂度
时间:
O
(
m
∗
∑
+
n
)
O(m * \sum + n)
O(m∗∑+n),其中
∑
\sum
∑为字符集大小
空间:
O
(
m
∗
∑
)
O(m * \sum)
O(m∗∑),动态规划数组
[hot] 416. 分割等和子集
题目
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。本题和<2. 数组划分成两个和相同的子数组>完全相同。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
题解
0,1背包问题,示例代码如下所示,dp[i]
代表是否nums中的数据,能否找到一个子序列,子序列和为i,因而dp[0] = true。
// 头文件
#include <algorithm>
#include <numeric>
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = accumulate(nums.begin(), nums.end(), 0);
int max_n = *max_element(nums.begin(), nums.end());
if (sum % 2 || max_n > sum / 2) {
return false;
}
int target = sum / 2;
vector<bool> dp(target + 1, false);
dp[0] = true;
for (auto n: nums) {
for (int i = target; i >= n; --i) {
dp[i] = dp[i] || dp[i - n];
if (dp[target]) {
return true;
}
}
}
return dp[target];
}
};
复杂度
时间复杂度:
O
(
n
∗
t
a
r
g
e
t
)
O(n * target)
O(n∗target)
空间复杂度:
O
(
t
a
r
g
e
t
)
O(target)
O(target)
关键点
转换问题,从是否能分成等和的两个子序列,转换为寻找目标和为sum/2的子序列
516. 最长回文子序列
题目
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
题解
示例代码如下所示:
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;
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];
}
};
复杂度
时间:
O
(
n
2
)
O(n^2)
O(n2)
空间:
O
(
n
2
)
O(n^2)
O(n2)
题解2
一维动态规划数组,其中 dp[j]
代表从i到j,最长的回文子序列长度,dp_pre[j]
代表的是从i+1到j,最长的回文子序列长度
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<int> dp(n, 1);
vector<int> dp_pre(dp);
for (int i = n - 2; i >= 0; --i) {
for (int j = i + 1; j < n; ++j) {
if (s[i] == s[j]) {
if (j == i + 1) {
dp[j] = 2;
} else {
dp[j] = dp_pre[j - 1] + 2;
}
} else {
dp[j] = max(dp_pre[j], dp[j - 1]);
}
}
dp_pre = dp;
}
return dp[n - 1];
}
};
复杂度
时间:
O
(
n
2
)
O(n^2)
O(n2)
空间:
O
(
n
)
O(n)
O(n)
关键点
深刻理解动态规划数组代表的含义
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 。
题解
动态规划数组dp,dp[i][j]表示text1[0:i]和text[0:j]的最长公共子串,递推公式如下所示:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 if c h a r i = c h a r j m a x ( d p [ i ] [ j − 1 ] , d p [ i − 1 ] [ j ] ) else dp[i][j]=\begin{cases} dp[i-1][j-1]+1 & \text{ if } chari=charj\\ max(dp[i][j-1], dp[i-1][j]) & \text{ else } \end{cases} dp[i][j]={dp[i−1][j−1]+1max(dp[i][j−1],dp[i−1][j]) if chari=charj else
示例代码如下所示:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
for (int i = 1; i <= m; ++i) {
auto& char_i = text1[i - 1];
for (int j = 1; j <= n; ++j) {
auto& char_j = text2[j - 1];
if (char_i == char_j) {
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];
}
};
复杂度
时间:
O
(
m
n
)
O(mn)
O(mn)
空间:
O
(
(
m
+
1
)
∗
(
n
+
1
)
)
O((m + 1) * (n + 1))
O((m+1)∗(n+1))
题解2
节省空间复杂度,浪费时间复杂度,直接定义m*n维矩阵,示例代码如下:
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size();
int n = text2.size();
vector<vector<int>> dp(m, vector<int>(n));
for (int i = 0; i < m; ++i) {
string c = to_string(text1[i]);
dp[i][0] = text2.find(c) != string::npos;
}
for (int j = 0; j < n; ++j) {
string c = to_string(text2[j]);
dp[0][j] = text1.find(c) != string::npos;
}
for (int i = 1; i < m; ++i) {
char ci = text1[i];
for (int j = 1; j < n; ++j) {
char cj = text2[j];
if (ci == cj) {
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 - 1][n - 1];
}
};
复杂度
时间:
O
(
(
m
+
1
)
∗
(
n
+
1
)
)
O((m + 1) * (n + 1))
O((m+1)∗(n+1))
空间:
O
(
m
n
)
O(mn)
O(mn)
野路子 不连续子数组
1. 判断子数组的任意子集的和是否有可能组成target
题解
定义动态规划数组dp,长度为target + 1,dp[i]代表的含义为数字i是否能够由数组nums中的任意元素组合相加得到。动态规划递推公式如下,其中num代表的含义为数组中的一个元素。
d
p
[
i
]
=
d
p
[
i
]
∣
∣
d
p
[
i
−
n
u
m
]
dp[i] = dp[i] || dp[i - num]
dp[i]=dp[i]∣∣dp[i−num]
算法执行的流程为
- dp[0] = true。
- 两层for循环,第一层为遍历数组中的元素num,第二层为从target开始逐次递减遍历,直到递减到num结束,具体执行过程见如下代码。
上述第二层遍历采用从大到小遍历的方案是由于避免数组元素中有且仅有一个元素的取值为target/2的情况出现。
class Solution {
public:
bool subarray_sum_to_target(std::vector<int> nums, int target) {
if (nums.empty()) {
return false;
}
vector<bool> dp(target + 1, false);
dp[0] = true;
for (const auto& num: nums) {
for (int i = target; i >= num; --i) {
dp[i] = dp[i] || dp[i - num];
if (dp[target] == true) {
return true;
}
}
}
return false;
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2),两层循环嵌套
空间复杂度:
O
(
n
)
O(n)
O(n),动态规划引入的额外存储空间
leetcode 连续子数组
[hot] 3. 无重复字符的最长子串
题目
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
题解
一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!如何移动?我们只要把队列的左边的元素移出就行了,直到满足题目要求!一直维持这样的队列,找出队列出现最长的长度时候,求出解!
示例代码如下所示:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int l = 0;
unordered_set<char> set;
int cur_len = 0;
int res = 0;
for (int i = 0; i < s.size(); ++i) {
++cur_len;
// 循环删除set中的元素,直到set里面不包含s[i]
while (set.count(s[i]) > 0) {
set.erase(s[l++]);
--cur_len;
}
res = max(cur_len, res);
set.insert(s[i]);
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
∑
)
O(\sum)
O(∑),
∑
\sum
∑代表的是字符集的大小
关键点
滑动窗口开始滑动时,弹出的是左侧元素,不是右侧
[hot] 5. 最长回文子串
题目
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
题解
完全套用【647. 回文子串】思路,添加边界值记录,示例代码如下:
class Solution {
public:
string longestPalindrome(string s) {
if (s.empty()) {
return s;
}
int length = s.size();
int start = 0;
int end = 0;
for (int i = 0; i < length * 2 - 1; ++i) {
int l = i / 2;
int r = i / 2 + i % 2;
while (l >= 0 && r < length && s[l] == s[r]) {
if (r - l > end - start) {
end = r;
start = l;
}
--l;
++r;
}
}
return s.substr(start, end - start + 1);
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
关键点
理解遍历回文子串的方式
[hot] 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
题解
动态规划公式如下,示例代码如下所示:
f
(
i
)
=
m
a
x
(
f
(
i
−
1
)
+
n
u
m
s
[
i
]
,
n
u
m
s
[
i
]
)
f(i) = max(f(i-1) + nums[i], nums[i])
f(i)=max(f(i−1)+nums[i],nums[i])
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int pre = 0;
int res = nums[0];
for (const auto& num: nums) {
pre = max(pre + num, num);
res = max(res, pre);
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
[hot] 152. 乘积最大子数组
题目
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。
题解
由于有负负得正的情况,因而需要分别记录最大值和最小值,示例代码如下所示:
class Solution {
public:
int maxProduct(vector<int>& nums) {
int max_f = nums[0];
int res = nums[0];
int min_f = nums[0];
for (int i = 1; i < (int)nums.size(); ++i) {
int max_f_b = max_f;
int min_f_b = min_f;
max_f = max(nums[i], max(nums[i] * max_f_b, nums[i] * min_f_b));
min_f = min(nums[i], min(nums[i] * max_f_b, nums[i] * min_f_b));
res = max(res, max_f);
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
关键点
记录上一次乘积最大值和最小值
209. 长度最小的子数组
题目
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
题解
双指针双层循环解决问题,示例代码如下所示:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int length = nums.size();
int low = 0;
int sum = 0;
int res = INT_MAX;
for (int i = 0; i < length; ++i) {
sum += nums[i];
while (sum >= target) {
res = min(res, i - low + 1);
sum -= nums[low++];
}
}
return res == INT_MAX ? 0 : res;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n),start和end最多移动n次
空间复杂度:
O
(
1
)
O(1)
O(1)
关键点
滑动窗口滑动时更新结果
485. 最大连续 1 的个数
题目
给定一个二进制数组, 计算其中最大连续 1 的个数。
示例:
输入:[1,1,0,1,1,1]
输出:3
解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3.
题解
遍历数组,nums[i]为1时拓展结果,不为1时缓存结果,示例代码如下所示:
class Solution {
public:
int findMaxConsecutiveOnes(vector<int>& nums) {
int length = nums.size();
if (length == 0) {
return 0;
}
int max_count = 0;
int count = 0;
for (int i = 0; i < length; ++i) {
if (nums[i] == 1) {
++count;
} else {
max_count = max(max_count, count);
count = 0;
}
}
// 可能出现最后有最大个数的1连续出现
return max(max_count, count);
}
};
复杂度
时间:O(N)
空间:O(1)
[hot] 560. 和为K的子数组
题目
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续子数组的个数。
示例 1 :
输入: nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
题解
本题与【39. 组合总和】不同的是,每个元素在组合中只能出现一次。本题关键点在于必须是连续子数组。前缀和方法解决问题,示例代码如下所示:
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> m;
m[0] = 1;
int pre = 0;
int res = 0;
for (const auto& num: nums) {
pre += num;
if (m.count(pre - k) > 0) {
res += m[pre - k];
}
++m[pre];
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
关键点
前缀和的初始化
[hot] 581. 最短无序连续子数组
题目
给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
请你找出符合题意的 最短 子数组,并输出它的长度。
示例 1:
输入:nums = [2,6,4,8,10,9,15]
输出:5
解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
示例 2:
输入:nums = [1,2,3,4]
输出:0
示例 3:
输入:nums = [1]
输出:0
题解
题解在 这里,示例代码如下:
排序后遍历:
#include <algorithm>
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
if (is_sorted(nums.begin(), nums.end())) {
return 0;
}
vector<int> numsSorted(nums);
sort(numsSorted.begin(), numsSorted.end());
int left = 0;
while (nums[left] == numsSorted[left]) {
left++;
}
int right = nums.size() - 1;
while (nums[right] == numsSorted[right]) {
right--;
}
return right - left + 1;
}
};
一次遍历:
class Solution {
public:
int findUnsortedSubarray(vector<int>& nums) {
int max_n = INT_MIN;
int min_n = INT_MAX;
int length = nums.size();
int left = -1, right = -1;
for (int i = 0; i < length; ++i) {
if (nums[i] >= max_n) {
max_n = nums[i];
} else {
right = i;
}
if (nums[length - i - 1] <= min_n) {
min_n = nums[length - i - 1];
} else {
left = length - i - 1;
}
}
return right == -1 ? 0 : right - left + 1;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
647. 回文子串
题目
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:"abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:"aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
题解
双指针一遍遍历解决问题。因为存在奇数长度和偶数长度两种情况,因而子回文串的中心个数为2 * n - 1(n为数组的长度)。示例代码如下所示:
class Solution {
public:
int countSubstrings(string s) {
int res = 0;
for (int i = 0; i < s.size() * 2 - 1; ++i) {
int l = i / 2;
int r = i / 2 + i % 2;
while (l >= 0 && r < s.size() && s[l] == s[r]) {
--l;
++r;
++res;
}
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
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
动态规划数组dp,dp[i]代表从i开始,连续递增数组的长度,转移方程为:
d p [ i ] = { d p [ i − 1 ] + 1 if n u m s [ i ] > n u m s [ i − 1 ] 1 else dp[i]=\begin{cases} dp[i-1]+1 & \text{ if } nums[i] > nums[i-1]\\ 1 & \text{ else } \end{cases} dp[i]={dp[i−1]+11 if nums[i]>nums[i−1] else
代码如下所示:
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int length = nums.size();
vector<int> dp(length, 1);
int res = 1;
for (int i = 1; i < length; ++i) {
if (nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
}
res = max(dp[i], res);
}
return res;
}
};
题解2
节省动态规划空间,示例代码如下所示:
class Solution {
public:
int findLengthOfLCIS(vector<int>& nums) {
int res = 1;
int temp = 1;
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] > nums[i - 1]) {
temp = temp + 1;
res = max(res, temp);
} else {
temp = 1;
}
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
718. 最长重复子数组 (最长公共子数组)
题目
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。
题解
从前到后动态规划,示例代码如下所示:
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
vector<vector<int>> dp(m, vector<int>(n));
int res = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (nums1[i] == nums2[j]) {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
res = max(res, dp[i][j]);
} else {
dp[i][j] = 0;
}
}
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
题解2
滑动窗口解决问题,
示例代码如下所示:
class Solution {
public:
int max_len_compute(vector<int>& nums1, vector<int>& nums2, int p1, int p2, int len) {
int ret = 0, k = 0;
for (int i = 0; i < len; ++i) {
if (nums1[p1 + i] == nums2[p2 + i]) {
++k;
} else {
k = 0;
}
ret = max(ret, k);
}
return ret;
}
int findLength(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
int ret = 0;
for (int i = 0; i < m; ++i) {
int len = min(n, m - i);
ret = max(ret, max_len_compute(nums1, nums2, i, 0, len));
}
for (int i = 0; i < n; ++i) {
int len = min(m, n - i);
ret = max(ret, max_len_compute(nums1, nums2, 0, i, len));
}
return ret;
}
};
复杂度
时间:
O
(
m
i
n
(
m
,
n
)
∗
(
m
+
n
)
)
O(min(m,n) * (m + n))
O(min(m,n)∗(m+n))
空间:
O
(
1
)
O(1)
O(1)
1013. 将数组分成和相等的三个部分
题目
给你一个整数数组 A,只有可以将其划分为三个和相等的非空部分时才返回 true,否则返回 false。
形式上,如果可以找出索引 i+1 < j 且满足 A[0] + A[1] + … + A[i] == A[i+1] + A[i+2] + … + A[j-1] == A[j] + A[j-1] + … + A[A.length - 1] 就可以将数组三等分。
示例 1:
输入:[0,2,1,-6,6,-7,9,1,2,0,1]
输出:true
解释:0 + 2 + 1 = -6 + 6 - 7 + 9 + 1 = 2 + 0 + 1
示例 2:
输入:[0,2,1,-6,6,7,9,-1,2,0,1]
输出:false
示例 3:
输入:[3,3,6,5,-2,2,5,1,-9,4]
输出:true
解释:3 + 3 = 6 = 5 - 2 + 2 + 5 + 1 - 9 + 4
题解
从前到后正常遍历解决问题,示例代码如下所示:
class Solution {
public:
bool canThreePartsEqualSum(vector<int>& arr) {
int cum = 0;
for (const auto& elem: arr) {
cum += elem;
}
if (cum % 3 != 0) {
return false;
}
int target = cum / 3;
int length = arr.size();
int cur = 0;
int i = 0;
while (i < length) {
cur += arr[i];
if (cur == target) {
break;
}
++i;
}
if (i >= length) {
return false;
}
int j = i + 1;
while (j + 1 < length) { // 保证最后一个子数组非空
cur += arr[j];
if (cur == target * 2) {
return true;
}
++j;
}
return false;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
1800. 最大升序子数组和
题目
给你一个正整数组成的数组 nums ,返回 nums 中一个 升序 子数组的最大可能元素和。
子数组是数组中的一个连续数字序列。
已知子数组 [numsl, numsl+1, …, numsr-1, numsr] ,若对所有 i(l <= i < r),numsi < numsi+1 都成立,则称这一子数组为 升序 子数组。注意,大小为 1 的子数组也视作 升序 子数组。
示例 1:
输入:nums = [10,20,30,5,10,50]
输出:65
解释:[5,10,50] 是元素和最大的升序子数组,最大元素和为 65 。
题解
滑动窗口解决问题,示例代码如下所示:
class Solution {
public:
int maxAscendingSum(vector<int>& nums) {
int pre = nums[0];
int ans = INT_MIN;
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] > nums[i - 1]) {
pre += nums[i];
} else {
ans = max(ans, pre);
pre = nums[i];
}
}
return max(ans, pre);
}
};
复杂度
时间:
O
(
n
)
O(n)
O(n)
空间:
O
(
1
)
O(1)
O(1)
剑指offer 连续子数组
42. 最大子序和,记录左右边界
实例代码如下:
class Solution {
public:
bool maximum_subarray(std::vector<int> nums, std::pair<int, int>* result) {
if (result == nullptr) {
return false;
}
int left = 0;
int right = 0;
int res = 0;
int cur_sum = 0;
for (int i = 0; i < nums.size(); ++i) {
if (cur_sum <= 0) {
cur_sum = nums[i];
left = i;
} else {
cur_sum += nums[i];
}
if (cur_sum > res) {
res = cur_sum;
right = i;
}
}
*result = {left, right};
return true;
}
};
42. 最小子序列和
题目
给定一个整数数组 nums ,找到一个具有最小和的连续子数组(子数组最少包含一个元素),返回其最小和。
题解
与最大子序列类似,最小子序列求解过程依然是边遍历边记录,示例代码如下:
class Solution {
public:
int minimum_subarray(std::vector<int> nums) {
if (nums.empty()) {
return -1;
}
int cur_sum = 0;
int result = 0;
for (const auto& num: nums) {
// 这里和最大子序列不同的是,cur_sum进循环时需要先累加,
// 因为如果这里不累加,cur_sum如果>0,则直接跳过了这一轮num对遍历过程的贡献
cur_sum += num;
if (cur_sum > 0) {
cur_sum = 0;
}
if (cur_sum < result) {
result = cur_sum;
}
}
// 找不到和的最小值,只能通过查看每个元素的大小来找最小值
if (result == 0) {
int min_num = INT_MAX;
for (const auto& num: nums) {
if (num < min_num) {
min_num = num;
}
}
result = min_num;
}
return result;
}
};
野路子 连续子数组
1. 连续子数组和的绝对值的最小值
题解
前缀和解决问题,具体思路如下:
- 遍历数组时中记录遍历过程中的累加和,并存储至sum_vec中
- 对sum_vec进行升序排序
- 排序后计算sum_vec相邻两个元素差值绝对值最小值,即为最终结果
class Solution {
public:
int sum_abs_minimum_subarray(std::vector<int> nums) {
if (nums.empty()) {
return -1;
}
int cur_sum = 0;
int result = INT_MAX;
vector<int> sum_vec;
for (const auto& elem: nums) {
cur_sum += elem;
// 已经有一组连续子数组触底了
if (cur_sum == 0) {
return 0;
}
sum_vec.push_back(cur_sum);
}
sort(sum_vec.begin(), sum_vec.end());
for (int i = 0; i < int(sum_vec.size() - 1); ++i) {
// 每个前缀和都是从0位置开始累加的,
// 因而sum_vec中任意两个元素相减一定是某个连续子数组之和
int temp_sub = abs(sum_vec[i + 1] - sum_vec[i]);
if (temp_sub < result) {
result = temp_sub;
}
}
return result;
}
};
复杂度
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
n
)
O(n)
O(n)
2. 连续子数组和的绝对值的最大值
题解
找到累加正值最大和累加负值最小,即可得到最终解。
示例代码如下
class Solution {
public:
int maxAbsoluteSum(vector<int>& nums) {
int p_max = 0, n_min = 0;
int p_sum = 0, n_sum = 0;
for (auto num : nums) {
p_sum += num;
p_sum = max(0, p_sum);
p_max = max(p_max, p_sum);
n_sum += num;
n_sum = min(0, n_sum);
n_min = min(n_min, n_sum);
}
return max(p_max, -n_min);
}
};
复杂度
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
n
)
O(n)
O(n)