利用滑动窗口+前缀和降低时间复杂度

滑动窗口+前缀和打开新世界

之前因为学校的课程设计,想坚持力扣每日一题真是难上加难,每天都是看看题解草草了事。现在课设结束了,复盘一下之前遇到的几个开拓眼界的题目。
题目来源:1703. 得到连续 K 个 1 的最少相邻交换次数
https://leetcode.cn/problems/minimum-adjacent-swaps-for-k-consecutive-ones/
大神讲解:【多图】新手教程,一步步带你写,把Hard分解成Easy
https://leetcode.cn/problems/minimum-adjacent-swaps-for-k-consecutive-ones/solution/duo-tu-xin-shou-jiao-cheng-yi-bu-bu-dai-6bps4/

下面是我的学习笔记


这个题目一眼看去,应该是要用贪心,但是具体怎么做,还是没有思路,然后看了大神们的思路,兴致勃勃去实践,在我以为双指针(为什么用双指针,因为我个人感觉滑动窗口就是双指针的另一种形式)可以解决问题的时候,忽然发现大神们竟然用了滑动窗口+前缀和,看了好久终于顿悟了!
这道题目思路:

  • 首先看题目是要找连续的1,第一想法肯定是想怎么将1移动到一起使得开销最少
  • 改进一下,我们不妨反向思维一下,把0向两边移动,1是不是就到一起了,用zeros数组记录两个1之间存在多少个0
  • 进而使用滑动窗口,滑动zeros数组计算总开销,注意如果移动zeros数组则是移动k-1个长度,比如:1010101,如果k是4的话,要把这4个1移动到一起,那么zeros窗口的长度就是3即k-1
  • 然后就到了最精彩的部分:前缀和,下面就来详细讲解一下大神是怎么用前缀和解决问题的

偷一下大神的图~这是样例
样例
然后计算第一个窗口的解
第一个窗口的开销(把0向两边移动,每个0路过几个1就乘几)为2*1 + 1*2 + 0*2 + 4*1
那么该怎么计算滑动窗口才能使复杂度为O(n)呢

我们要利用滑动窗口的特性,即:下一个窗口的解,可以由前一个窗口的解快速得到!

前缀和可以做到这一点

在这里插入图片描述

假设当前的窗口从 i 到 j ,那么上一个窗口就是从 i-1 到 j-1 。我们可以找到一个中点mid,它的cost是不变的,它左边的cost都减少了1,而右边的cost都增加了1。这就是本题的核心。
因为减少/增加的1需要与zeros中的值相乘,所以cost的变化可以通过求zeros上「区间和」来快速得到。

一句话点明整个题目!
然后区分一下窗口长度奇偶的情况
在这里插入图片描述
因此zeros数组从头滑动到尾就可以解决问题了,实现了O(n)时间复杂度,比双指针快了很多。
放一下大神的代码:

class Solution {
private:
    vector<int> zeros;
    vector<int> pre {0};
    
    void GenerateZeros(const vector<int> &nums) {
        int n = nums.size(), i = 0;
        while (i < n && nums[i] == 0) i++;
        while (i < n) {
            int j = i+1;
            while (j < n && nums[j] == 0) j++;
            if (j < n) zeros.push_back(j-i-1);
            i = j;
        }
    }
    
    //计算前缀和,即前面提到的下一个窗口的解,可以由前一个窗口的解快速得到
    void GeneratePresum(vector<int>& zeros) {
        for (int i = 0; i < zeros.size(); i++) {
            pre.push_back(pre.back() + zeros[i]);
        }
    }
    
    //利用前缀和很容易得到你想要的区间和,进而决定加或者减
    int GetRangeSum(int left, int right) {
        return pre[right+1] - pre[left];
    }
    
public:
    int minMoves(vector<int>& nums, int k) {
        GenerateZeros(nums);                        //第一步:生成zeros
        
        int cost = 0;                               //第二步:计算第一个窗口的解
        int left = 0, right = k-2;
        for (int i = left; i <= right; i++) {
            cost += zeros[i] * (min(i+1, right-i+1));
        }
        
        int minCost = cost;
        
        GeneratePresum(zeros);                      //第三步:开始滑动窗口
        int i = 1, j = i + k - 2;
        for (; j < zeros.size(); i++, j++) {
            int mid = (i + j) / 2;
            cost -= GetRangeSum(i-1, mid-1);
            cost += GetRangeSum(mid+k%2, j);
            minCost = min(minCost, cost);
        }
        
        return minCost;
    }
};

🍅

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值