一、滑动窗口
1、滑动窗口移动模板
对于滑动窗口算法,在解决一些子数组、子字符串问题比较常用,能够有效降低时间复杂度。该算法的关键是不断滑动,每次滑动都要维护好(更新)窗口内的状态,根据条件更新所需答案。下面给出常用的滑动窗口的伪代码模板,以字符串为例
int left = 0, right = 0;
while (right < s.size()) {
// 记录右端点
char r = s[right];
// 右端点 右移 增大窗口
right++;
// 更新窗口内状态
....
// 判断是否需要收缩窗口
while (window needs shrink) {
// 根据条件,更新所需答案
...
// 记录左端点
char L = s[left];
// 窗口收缩,左端点右移
left++;
// 更新窗口内状态
...
}
}
模板里每次记录左、右端点是为了在更新窗口内状态使用。如何滑动比较明确,每个情形都差不多,易错点是如何维护窗口内的状态。在解决滑动窗口类问题,每次都需要明确的是窗口状态是什么。
下面以几道题进行说明。
2、长度最小子数组 209
要求找到数组中满足其总和大于等于 target 的长度最小的 子数组。暴力做法是枚举以每个位置为起点、满足总和大于等于 target的子数组,这时需要两个for循环,时间复杂度是
O
(
N
2
)
O(N^2)
O(N2)。既然是子数组的问题,可以考虑使用滑动窗口。
窗口状态是什么?这题是窗口内的总和。
根据模板写出的代码如下,其实细看代码还是模板里那些步骤:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0, right = 0; // 滑动窗口左、右端点
int s = 0, Len = INT_MAX; // 记录窗口内的状态,子数组累加和、长度
while(right<nums.size()){
s += nums[right++]; // 记录当前滑动窗口右端点,记录完后将窗口右移
// 判断窗口是否需要收缩
while(s >= target){
// 更新子数组长度
Len = min(Len, right-left);
s -= nums[left++]; // 窗口左端点收缩
}
}
return Len < INT_MAX ? Len : 0;
}
};
C++另一种写法是将运用for循环,看个人习惯(注意这两种写法right的更新时间不一样)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0; // 滑动窗口左端点
int s = 0, Len = INT_MAX; // 记录窗口内状态,子数组累加和、长度
for(int right=0; right<nums.size(); ++right){ // 滑动窗口右端点
s += nums[right]; // right++ 在for最后自动完成
// 判断数组是否需要收缩
while(s >= target){
// 更新子数组长度
Len = min(Len, right-left+1);
s -= nums[left++]; // 窗口左端点收缩
}
}
return Len < INT_MAX ? Len : 0;
}
};
3、细化:滑动窗口子串问题模板
滑动窗口子串问题模板(出处参考资料1,非原创)
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
运用这个模板能够轻松解决子串问题。
4、最小覆盖子串 76
这在LeetCode是道困难题,但不要被困难两个字吓到,运用上面的模板可以轻松解决。
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> window, need;
for(char p:t)
need[p]++;
int left = 0, right = 0, cnt = 0;
int start = 0, Len = INT_MAX;
while(right<s.size()){
char ss = s[right++];
//更新窗口内信息
if(need.count(ss)){
window[ss]++;
if(window[ss]==need[ss])
++cnt;
}
//判断是否需要收缩左窗口
while(cnt==need.size()){
if(Len>right-left){
start = left;
Len = right - left;
}
ss = s[left++];
if(need.count(ss)){
if(window[ss]==need[ss])
--cnt;
--window[ss];
}
}
}
return Len<INT_MAX ? s.substr(start, Len) : "";
}
};
看吧,明白了套路后,滑动窗口类的问题就显得比较简单了。
与这题类似的有438.找到字符串中所有字母异位词
代码也贴上来,对比一下,是不是代码几乎一模一样,这就是套模板的好处。(原作的功劳,速速给原作者去点赞)
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
unordered_map<char, int> need, window;
for(char pp:p)
need[pp]++;
// 左窗口、右窗口、计数
int left = 0, right = 0, cnt = 0, k = p.length();
vector<int> ans;
while(right < s.length()){
//窗口右移
char ss = s[right];
right++;
//更新窗口内容
if(need.find(ss)!=need.end()){
window[ss]++;
if(window[ss]==need[ss])
cnt++;
}
//判断窗口是否需要收缩
while(right-left>=k){
if(cnt==need.size())
ans.push_back(left);
//左窗口收缩
ss = s[left];
left++;
if(need.find(ss)!=need.end()){
if(window[ss]==need[ss])
cnt--;
window[ss]--;
}
}
}
return ans;
}
};
二、螺旋矩阵问题
1、螺旋矩阵 54
已知数组,要求按顺时针螺旋顺序返回所有值。需要耐心,模拟顺时针螺旋的过程。按照从左到右、从上到下、从右到左、从下到上的顺序模拟,每模拟完一个方向要更新一下边界,表示已经遍历过。
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int left = 0, right = matrix[0].size() - 1, up = 0, down = matrix.size() - 1;
vector<int> ans;
while(true){
//从左到右
for(int i=left; i<=right; ++i)
ans.push_back(matrix[up][i]);
++up;
if(up > down)
break;
//从上到下
for(int i=up; i<=down; ++i)
ans.push_back(matrix[i][right]);
--right;
if(left > right)
break;
//从右到左
for(int i=right; i>=left; --i)
ans.push_back(matrix[down][i]);
--down;
if(up > down)
break;
//从下到上
for(int i=down; i>=up; --i)
ans.push_back(matrix[i][left]);
++left;
if(left > right)
break;
}
return ans;
}
};
2、螺旋矩阵Ⅱ
与上一题几乎一模一样,还是一模一样的模拟过程
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> ans(n, vector<int>(n));
int left = 0, right = n - 1, up = 0, down = n - 1;
int value = 1;
while(true){
//从左到右
for(int i=left; i<=right; ++i)
ans[up][i] = value++;
++up;
if(up > down)
break;
//从上到下
for(int i=up; i<=down; ++i)
ans[i][right] = value++;
--right;
if(left > right)
break;
//从右到左
for(int i=right; i>=left; --i)
ans[down][i] = value++;
--down;
if(up > down)
break;
//从下到上
for(int i=down; i>=up; --i)
ans[i][left] = value++;
++left;
if(left > right)
break;
}
return ans;
}
};
三、前缀和方法
前缀和方法是解决区间和问题的经典做法,用空间换时间。今天只做简单的介绍
1、区间和
以这题为例,有数组
n
u
m
s
[
0
]
,
.
.
.
,
n
u
m
s
[
n
−
1
]
nums[0],...,nums[n-1]
nums[0],...,nums[n−1],想知道任意区间的区间和,我们得遍历这个区间求和。
前缀和数组概念:如果我们有一个前缀和数组
p
r
e
_
s
u
m
pre\_sum
pre_sum,
p
r
e
_
s
u
m
[
i
]
pre\_sum[i]
pre_sum[i]记录着数组
n
u
m
s
nums
nums从第0个位置到第i个位置的累加和(都是以第0个位置为起点,所以称为前缀和),那么给定任意区间
[
i
,
j
]
(
i
<
=
j
)
[i,j](i<=j)
[i,j](i<=j),我们需要求解
n
u
m
s
[
i
]
,
n
u
m
s
[
i
+
1
]
,
.
.
.
,
n
u
m
s
[
j
]
nums[i], nums[i+1],..., nums[j]
nums[i],nums[i+1],...,nums[j]的和,我们只需要使用
p
r
e
_
s
u
m
[
j
]
(
n
u
m
s
[
0
]
+
n
u
m
s
[
1
]
+
.
.
.
+
n
u
m
s
[
i
]
+
n
u
m
s
[
i
+
1
]
+
.
.
.
+
n
u
m
s
[
j
]
)
pre\_sum[j](nums[0]+nums[1]+...+nums[i]+nums[i+1]+...+nums[j])
pre_sum[j](nums[0]+nums[1]+...+nums[i]+nums[i+1]+...+nums[j])减去
p
r
e
_
s
u
m
[
i
−
1
]
(
n
u
m
s
[
0
]
+
n
u
m
s
[
1
]
+
.
.
.
+
n
u
m
s
[
j
−
1
]
)
pre\_sum[i-1](nums[0]+nums[1]+...+nums[j-1])
pre_sum[i−1](nums[0]+nums[1]+...+nums[j−1])就可得到。
C++需要注意的一点是
i
=
0
i=0
i=0的情况,
p
r
e
_
s
u
m
[
i
−
1
]
pre\_sum[i-1]
pre_sum[i−1]会out of range,要么写个if条件判断,要么在
p
r
e
_
s
u
m
pre\_sum
pre_sum数组开头添加一个0(此时
p
r
e
_
s
u
m
pre\_sum
pre_sum数组含义为
p
r
e
_
s
u
m
[
i
]
pre\_sum[i]
pre_sum[i]记录着数组
n
u
m
s
nums
nums从第0个位置到第i-1个位置的累加和)。我比较喜欢第二种,代码更短一点。
这题代码如下
#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> arr(n, 0);
vector<int> sumArr(n+1, 0);
int sum = 0;
for (int i = 0; i < n; ++i) {
cin >> arr[i];
sum += arr[i];
sumArr[i+1] = sum;
}
int a, b, result;
while (cin >> a >> b) {
result = sumArr[b+1] - sumArr[a];
std::cout << result ;
cout<<endl;
}
return 0;
}
2、开发商购地
代码如下
#include<iostream>
#include<vector>
#include <climits>
using namespace std;
int main(){
int ans = INT_MAX;
int row, col;
cin>>row>>col;
vector<vector<int>>matrix(row, vector<int>(col));
int s = 0;
// 统计行、列的和
vector<int> sum_row(row, 0), sum_col(col, 0);
for(int i=0; i<row; ++i){
for(int j=0; j<col; ++j){
cin>>matrix[i][j];
sum_row[i] += matrix[i][j];
sum_col[j] += matrix[i][j];
s += matrix[i][j];
}
}
// 开始遍历分割线
// 先遍历按行分割
int pre_sum_row = 0; // 用于记录每行的前缀和
for(int i=1; i<row; ++i){
pre_sum_row += sum_row[i-1];
ans = min(ans, abs(s - 2 * pre_sum_row));
}
int pre_sum_col = 0; // 用于记录每列的前缀和
for(int i=1; i<col; ++i){
pre_sum_col += sum_col[i-1];
ans = min(ans, abs(s - 2 * pre_sum_col));
}
cout << ans << endl;
return 0;
}
还有一些其它场景下的使用,待补全
四、写在最后
今天最大的感受就是 把已经掌握的知识逻辑清楚、条理清晰的讲出来是需要花一些心思和时间在表达上进行构思的。加油~希望自己坚持下去。前缀和日后单独出一个博客来详细总结一下,今天没时间了。
参考资料
[1] 我写了一首诗,把滑动窗口算法变成了默写题
[2] 代码随想录