单调队列-详细讲解(含例题)

定义:

顾名思义,单调队列的重点分为「单调」和「队列」。

「单调」指的是元素的「规律」——递增(或递减)。

「队列」指的是元素只能从队头和队尾进行操作。

作用:

用于求解区间最值,比如滑动窗口类问题,时间复杂度为 O ( n ) O(n) O(n)

实现:

维护一个双向队列(deque),

① 加入元素时,从队尾依次删除比该元素贡献更小、代价更大不再需要的元素。

② 如果对元素有其他约束条件,需要在队头删除不满足条件的元素。

③ 通常队头元素就是当前队列维护的最优解

理解并记住上面的性质,就可以做题巩固了。

例题介绍

239 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
1 [3  -1  -3] 5  3  6  7       3
1  3 [-1  -3  5] 3  6  7       5
1  3  -1 [-3  5  3] 6  7       5
1  3  -1  -3 [5  3  6] 7       6
1  3  -1  -3  5 [3  6  7]      7

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

该题是单调队列的典型例题,要求定长为k的区间最大值,区间元素有进有出。

我们维护一个双向队列(deque),让队头元素代表的整数始终为队列的最大值。

步骤是顺序地依次加入每个下标元素,这样就将约束条件转化为了队列内的每个下标到当前下标的距离不能小于等于k。

对于当前要加入的每个下标元素i,我们将它认为是区间的右端点,现在我们要尽可能地删除队列内贡献更小、代价更大的元素:

显然的是当前元素i比队内所有元素到之后加入的元素的距离更小,即代价更小(必定),

如果nums[i]比队尾元素代表的整数更大,则nums[i]的贡献更大,则队尾元素不可能是区间的最大值了,因为nums[i]的贡献更大、代价更小,始终能取代它,因此需要删除该队尾,直到队列为空或者nums[i]的贡献没有比队尾的大,最后才将元素i加入队列中。

同时,在新的队列中,我们需要检查队列元素是否符合约束条件:

如果队头元素到当前元素i的距离大于k,则不满足在长度为k的区间内,是必须删除的。

如果当前元素i已经大于等于k - 1了,说明区间长度已经达到k,需要记录答案了。

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int>ans;
        deque<int>q;
        int n = nums.size();
        for(int i = 0; i < n; i++){
            while(!q.empty() && nums[q.back()] <= nums[i]){
                q.pop_back();
            }
            q.push_back(i);
            while(q.front() <= i - k){
                q.pop_front();
            }
            if(i >= k - 1){
                ans.push_back(nums[q.front()]);
            }
        }
        return ans;
    }
};
夯实进阶

利用单调队列来优化动态规划的状态转移

1425 带限制的子序列和

给你一个整数数组 nums 和一个整数 k ,请你返回 非空 子序列元素和的最大值,子序列需要满足:子序列中每两个 相邻 的整数 nums[i]nums[j] ,它们在原数组中的下标 ij 满足 i < jj - i <= k

数组的子序列定义为:将数组中的若干个数字删除(可以删除 0 个数字),剩下的数字按照原本的顺序排布。

示例 1:

输入:nums = [10,2,-10,5,20], k = 2
输出:37
解释:子序列为 [10, 2, 5, 20] 。

提示:

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

本题容易想到动态规划的解法,定义$ f[i]$ 表示在数组的前 i i i 个数中进行选择,并且恰好选择了第 i i i个数,可以得到的最大和。

f [ i ] f[i] f[i]的转移有:①只选择nums[i], 即 f [ i ] = n u m s [ i ] f[i]=nums[i] f[i]=nums[i]

②选择了之前的数,则我们需要枚举这个数来进行转移,即 f [ i ] = m a x j < i ( f [ j ] ) + n u m s [ i ] , i − j ≤ k f[i]=\underset{jf[i]=j<imax(f[j])+nums[i],ijk

这样做的时间复杂度为 O ( n k ) O(nk) O(nk),会超时,需要进行优化。

有聪明的同学可能已经看出来了,②中的枚举操作与前一题滑动窗口最大值是异曲同工的,就是在以i为右端点,长度为k的区间中求最大值。

在这里,使用单调队列的一般步骤:去头→择优→去尾→加入

class Solution {
public:
    int constrainedSubsetSum(vector<int>& nums, int k) {
        int n = nums.size();
        
        vector<int>f(n);
        f[0] = nums[0];

        deque<int>q;
        q.push_back(0);

        for(int i = 1; i < n; i++){
            while(!q.empty() && i - q.front() > k) q.pop_front(); // 去头

            f[i] = max(f[q.front()],0) + nums[i]; // 择优

            while(!q.empty() && f[i] >= f[q.back()]) q.pop_back(); // 去尾
            
            q.push_back(i); // 加入
        }
        return *max_element(f.begin(),f.end());
    }
};
相关题目

和至少为 K 的最短子数组

满足不等式的最大值

跳跃游戏 VI

这里给大家分享些计算机经典书籍推荐(含下载方式)

有用的话点个赞吧~关注@曾续缘,持续获取更多资料。

你可能感兴趣的:(数据结构与算法,算法,数据结构)