【Day2 LeetCode】滑动窗口、矩阵模拟、前缀和

一、滑动窗口

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[n1],想知道任意区间的区间和,我们得遍历这个区间求和。
前缀和数组概念:如果我们有一个前缀和数组 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[i1](nums[0]+nums[1]+...+nums[j1])就可得到。
C++需要注意的一点是 i = 0 i=0 i=0的情况, p r e _ s u m [ i − 1 ] pre\_sum[i-1] pre_sum[i1]会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] 代码随想录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值