滑动窗口题集1

1 滑动窗口

priority_queue经常用

0480 滑动窗口中位数

1 题目

https://leetcode-cn.com/problems/sliding-window-median/

2 解题思路

  • 1 使用一个multiset维护当前窗口,
    • 1.1 不使用priority_queue的原因,无法删除元素
    • 1.2 不使用map/set的原因,不能含有重复元素
  • 2 对于窗口,维护一个中位数指针,注意到中位数指针在每一次窗口移动只会发生几种情况
    • 2.1 向左,向右,不动
    • 2.2 分类讨论清除即可
class Solution {
public:
    vector<double> medianSlidingWindow(vector<int>& nums, int k) {
        long long n = nums.size();
        std::multiset<long long, std::less<long long>> mySet(nums.begin(), nums.begin() + k);

        vector<double> res;
        multiset<long long>::iterator mid = mySet.begin();
        std::advance(mid, (k-1)/2);

        for(long long i = k; i <= n; ++i) {
            res.emplace_back((*mid + *next(mid, 1 - k%2))*1.0L/2);
            if(i == n) break;

            mySet.insert(nums[i]);
            if (nums[i] > *mid && nums[i - k] < *mid) {
                mid++;
                mySet.erase(mySet.lower_bound(nums[i-k]));
                continue;
                // std::advance(mid, 1);
            }

            if (nums[i] < *mid && nums[i - k] > *mid) {
                mid--;
                mySet.erase(mySet.lower_bound(nums[i-k]));
                continue;
                // std::advance(mid, -1);
            }
            // 7 3 7 7 4, k = 4
            // 7 8 7 7 4, k = 4
            if(nums[i-k] == *mid) {
                if(nums[i] >= *mid)  ++mid;
                else {
                    if(*prev(mid) != *mid) {
                        --mid;
                    }
                }
                mySet.erase(mySet.lower_bound(nums[i-k]));
                continue;
            } 

            if(nums[i] == *mid) {// 相当于一个比mid大的数字插入到了mid的前面
                if(nums[i-k] <= *mid)  ++mid;
                mySet.erase(mySet.lower_bound(nums[i-k]));
                continue;
            }
        }
        return res;
    }
};

0992 k个不同元素的子数组个数

1 题目

https://leetcode-cn.com/problems/subarrays-with-k-different-integers/submissions/

2 解题思路

  • 1 正常思路:
    • 1.1 首先窗口是必须的,即为[st, ed],那么保证这个窗口时刻含有k个不同变量,然后求出来每个以ed为结尾的子数组的个数求和即可
    • 1.2 那么以ed为结尾的窗口[st, ed]的子数组个数求法,假设k=2,窗口为1,2,1,2,那么以ed为结尾,st就向前移动,直到窗口内的不同元素个数减少到了k-1,此时st移动到第二个2的位置,一共移动了3次,也就是说以ed为结尾的含有k个不同变量的子数组个数为3。
    • 1.3 其中的复杂之地在于:如何判断窗口内不同元素的个数,我们采用经典的空间换时间的方法(因为所有元素的值不会大于数组本身长度),用freq[val]记录val出现的次数, 倘若长度不限呢?那就需要使用unordered_map来记录当前窗口所有元素的出现次数,然后每移动一次st需要遍历一遍这个map来判断当前窗口内不同元素的个数,那么整体复杂度为: o(n * k * k)
  • 2 官方题解:
    • 2.1 不同元素为k的子数组的个数为: 不同元素最多为k的子数组个数 - 不同元素最多为k-1的子数组个数,那么问题转为求不同元素最多为k的一个数组它子数组的个数
    • 2.2 求法: 还是滑动窗口的思想,始终保持窗口中最多元素的个数不超过k(方式为每次移动ed,直到第一次超过k,然后移动st直到小于k),然后对于每个ed,ed - st就是以ed为窗口结尾对应的不同元素不超过k的子数组的个数,举个例子:(官方例子):

用具体的例子理解:最多包含 3 种不同整数的子区间 [1, 3, 2, 3] (双指针算法是在左边界固定的前提下,让右边界走到最右边),当前可以确定 1 开始的满足最多包含 3 种不同整数的子区间有 [1]、[1, 3]、[1, 3, 2]、[1, 3, 2, 3]。

class Solution {
public:
    int subarraysWithKDistinct(vector<int>& nums, int k) {
        return maxSubArrayNumForKDiff(nums, k) - maxSubArrayNumForKDiff(nums, k - 1);

    }

    int maxSubArrayNumForKDiff(vector<int>& nums, int k) {
        vector<int> freq(nums.size() + 1);
        long long res = 0;
        int st = 0;
        int ed = 0;
        int curCnt = 0;
        while(ed < nums.size()) {
            // 求每个ed对应得到的最多k个不同元素的子数组个数
            if(freq[nums[ed]] == 0) {
                curCnt ++;
            }
            freq[nums[ed]]++;
            ++ed;

            // 减小窗口到窗口内元素种类第一次为k-1个
            while(curCnt > k) {
                freq[nums[st]]--;
                if(freq[nums[st]] == 0) {
                    curCnt--;
                }
                ++st;
            }
            res += ed - st;
        }
        return res;
    }
};

0904 完全水果个数

1 题目

https://leetcode-cn.com/problems/fruit-into-baskets/

2 解题思路

  • 1 正常思路:(于0904https://leetcode-cn.com/problems/fruit-into-baskets/实现)
    • 1.1 首先窗口是必须的,即为[st, ed],那么保证这个窗口时刻含有k个不同变量,然后求出来每个以ed为结尾的子数组的个数求和即可
    • 1.2 那么以ed为结尾的窗口[st, ed]的子数组个数求法,假设k=2,窗口为1,2,1,2,那么以ed为结尾,st就向前移动,直到窗口内的不同元素个数减少到了k-1,此时st移动到第二个2的位置,一共移动了3次,也就是说以ed为结尾的含有k个不同变量的子数组个数为3。
    • 1.3 其中的复杂之地在于:如何判断窗口内不同元素的个数,我们采用经典的空间换时间的方法(因为所有元素的值不会大于数组本身长度),用freq[val]记录val出现的次数, 倘若长度不限呢?那就需要使用unordered_map来记录当前窗口所有元素的出现次数,然后每移动一次st需要遍历一遍这个map来判断当前窗口内不同元素的个数,那么整体复杂度为: o(n * k * (log k)) = o(n)
  • 2 官方题解:
    • 2.1 不同元素为k的子数组的个数为: 不同元素最多为k的子数组个数 - 不同元素最多为k-1的子数组个数,那么问题转为求不同元素最多为k的一个数组它子数组的个数
    • 2.2 求法: 还是滑动窗口的思想,始终保持窗口中最多元素的个数不超过k(方式为每次移动ed,直到第一次超过k,然后移动st直到小于k),然后对于每个ed,ed - st就是以ed为窗口结尾对应的不同元素不超过k的子数组的个数,举个例子:(官方例子):

用具体的例子理解:最多包含 3 种不同整数的子区间 [1, 3, 2, 3] (双指针算法是在左边界固定的前提下,让右边界走到最右边),当前可以确定 1 开始的满足最多包含 3 种不同整数的子区间有 [1]、[1, 3]、[1, 3, 2]、[1, 3, 2, 3]。

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        if(fruits.size() <= 1) {
            return 1;
        }

        // 不同元素最多为2的数组长度
        int res = 0;
        int st = 0;
        int ed = 0;
        int curCnt = 0;
        map<int, int> freq;
        bool stNotMov = true;
        while(ed < fruits.size()) {
            while(freq.size() <= 2 && ed < fruits.size()) {
                if(freq.find(fruits[ed]) == freq.end()) {
                    curCnt++;
                }
                freq[fruits[ed]]++;
                ed++;
            }
            
            if(!stNotMov && ed != fruits.size()) {
                res = std::max(res, ed - st - 1);
            } else  {
                if(freq.size() == 3) {
                    res = std::max(res, ed - st - 1);
                } else {
                    res = std::max(res, ed - st);
                }
                
            }
            
            while(freq.size() > 2) {
                freq[fruits[st]]--;
                if(freq[fruits[st]] == 0) {
                    freq.erase(freq.find(fruits[st]));
                }
                ++st;
                stNotMov = false;
            }
        }
        return res;
    }
};

0995 执行次数最小: 每次翻转k个让数组全为1

1 题目

https://leetcode-cn.com/problems/minimum-number-of-k-consecutive-bit-flips/

2 解题思路

  • 1 正常思路:窗口始终保持k个,去模拟翻转过程,由于需要在窗口内找到翻转后第一个为0的数字,这个复杂度为O(k),所以总复杂度为: O(n*k)
  • 2 官方题解:使用差分数组diff,diff[i]表示nums[i]比nums[i-1]多了多少次翻转,那么当前总的翻转次数就为: sum(diff[i]),对于nums[i]而言, sum(diff[i])%2为0则表示nums[i]需要翻转。
class Solution {
public:
    int minKBitFlips(vector<int>& nums, int k) {
        vector<long long> preSum = {0};
        for(long long num : nums) {
            preSum.emplace_back(preSum.back() + num);
        }

        long long st = 0;
        long long ed = st + k - 1;
        long long n = nums.size();
        long long res = 0;

        vector<long long> diff(n+1);
        long long flipCnt = 0;
        // while(st < n - k + 1) {
            // 模拟思路会超时
            // if(nums[st] == 1) {
            //     ++st;
            //     continue;
            // }
            // int newSt = flipKBit(nums, k, st, preSum);
            // if(newSt == -1) {
            //     st = st + k;
            // } else {
            //     st = newSt;
            //     res += 1;
            // }
        // }
        // if(find(nums.end() - k, nums.end(), 0) == (nums.end())) {
        //     return res;
        // } 

        while(st < n) {
            // 采用查分数组记录每个元素应该翻转的次数
            // 这启发我们用差分数组的思想来计算当前数字需要翻转的次数。我们可以维护一个差分数组 \textit{diff}diff,其中 \textit{diff}[i]diff[i] 表示两个相邻元素 
            // \textit{nums}[i-1]nums[i−1] 和 \textit{nums}[i]nums[i] 的翻转次数的差,对于区间 [l,r][l,r],将其元素全部加 11,只会影响到 ll 和 r+1r+1 处的差分值,
            // 故 \textit{diff}[l]diff[l] 增加 11,\textit{diff}[r+1]diff[r+1] 减少 11。
            flipCnt += diff[st];
            if((flipCnt + nums[st]) % 2 == 0) {
                if(st + k > n) {
                    return -1;
                }
                diff[st] ++;
                diff[st + k] --;
                res++;
                flipCnt ++;
            }
            ++st;
        }
        return res;
    }

    // 翻转kbit,返回第一个翻转窗口中反转后值不等于1的下标,否则返回-1
    int flipKBit(vector<int>& nums, int k, int st, vector<int>& preSum) {
        int firstNot1 = INT_MAX;
        // 需要O(k)时间的复杂度
        bool needFlip = find(nums.begin() + st, nums.begin() + st + k, 0) != (nums.begin() + st + k);
        // 使用前缀和优化,由于实地翻转了数组,于是会改变对应的前缀和,所以此方法行不通
        // bool needFlip = ((preSum[st + k] - preSum[st]) != k);

        // for(int i = st; i < k; ++i) {
        //     if(nums[st + i] != 1) {
        //         needFlip = true;
        //     }
        // }
        if(needFlip) {
            for(int i = 0; i < k; ++i) {
                nums[st + i] = abs(nums[st + i] - 1);
                if(nums[st + i] != 1) {
                    firstNot1 = min(firstNot1, st + i);
                }
            }
            return firstNot1 > nums.size() ? st + k : firstNot1 ;
        }
        

        return -1;
    }
};

1696 最大结果

1 题目

https://leetcode-cn.com/problems/jump-game-vi/submissions/

2 解题思路

  • 1 采用动态规划:dp[i]表示跳到i处的最大收益,如下:搜索i的前k个下标即可
        // o(n * k)解法
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i = 1; i < nums.size() + 1; ++i) {
            for(int m = 1; m < k + 1 && i - m > 0; ++m) {
                dp[i] = max(dp[i], dp[i-m] + nums[i-1]);
            }
        }
  • 2 优化上述搜索前k个下标的方案,我们采用优先队列来维护前k个中最大的上一跳:
    • 将上述O(n*k)变为O(n*logk),原因是maxHeap的push操作是logN的复杂度
class Solution {
public:
    struct number {
        long long idx = 0;
        long long val = 0;
        number(long long idx, long long val): idx(idx), val(val) {};

        // sort descending
        bool  operator<(const number& b) const {return this->val < b.val;}
    };
    int maxResult(vector<int>& nums, int k) {
        vector<long long> dp(nums.size() + 1, INT_MIN);
        // o(n * k)解法
        dp[0] = 0;
        dp[1] = nums[0];
        // for(int i = 1; i < nums.size() + 1; ++i) {
        //     for(int m = 1; m < k + 1 && i - m > 0; ++m) {
        //         dp[i] = max(dp[i], dp[i-m] + nums[i-1]);
        //     }
        // }

        std::priority_queue<number, std::vector<number>, std::less<number>> maxHeap;
        maxHeap.push({1, nums[0]});
        // 使用堆优化:
        for(int i = 2; i < nums.size() + 1; ++i) {
            while(maxHeap.top().idx < i - k) {
                maxHeap.pop();
            }

            dp[i] =  maxHeap.top().val + nums[i - 1];
            maxHeap.push({i, dp[i]});
            
            // for(int m = 1; m < k + 1 && i - m > 0; ++m) {
            //     dp[i] = max(dp[i], dp[i-m] + nums[i-1]);
            // }
        }
        
        return dp[nums.size()];

    }
};

总结

滑动窗口的最大值:可以使用maxHeap来维护,复杂度logk

上一篇:二分图博弈学习笔记


下一篇:MySQL异步驱动aiomysql