Kiner算法刷题记(十三):单调队列(手撕算法篇)

系列文章导引

开源项目

本系列所有文章都将会收录到GitHub中统一收藏与管理,欢迎ISSUEStar

GitHub传送门:Kiner算法算题记

239. 滑动窗口最大值

解题思路

求取滑动窗口最大值,由于滑动窗口问题实际上就是固定结尾的RMQ问题,我们可以使用单调队列解决,而要求解的是最大值,因此应该使用单调递增队列。

代码演示

/*
 * @lc app=leetcode.cn id=239 lang=typescript
 *
 * [239] 滑动窗口最大值
 */

// @lc code=start
type Data = {
    idx: number,
    data: number
};
function maxSlidingWindow(nums: number[], k: number): number[] {
    // 求取滑动窗口最大值,由于滑动窗口问题实际上就是固定结尾的RMQ问题,我们可以使用单调队列解决,
    // 而要求解的是最大值,因此应该使用单调递增队列
    let res: number[] = [];
    let q:Data[] = [];
    for(let i=0;i<nums.length;i++) {
        // 将队列尾部多有违反队列单调性的元素都弹出队列
        while(q.length && q[q.length-1].data < nums[i]) q.pop();
        // 将当前元素加入到队列当中
        q.push({idx: i, data: nums[i]});
        // 如果队列头部元素移出了滑动窗口,则将该元素弹出
        if(i - q[0].idx === k) q.shift();
        // 如果滑动窗口无法再向后滑动,则继续
        if(i+1<k) continue;
        // 此时队列头部的元素就是滑动窗口的最大值
        res.push(q[0].data);
    }
    return res;
};
// @lc code=end


剑指 Offer 59 - II. 队列的最大值

解题思路

这道题跟普通队列唯一的不同就是需要使用均摊复杂度为O(1)的方法获取队列中的最大值,我们可以将整个队列看成一个大的滑动窗口,那么,这道踢就变成了一个长度为队列长度的RMQ问题了,要求最大值,则我们需要一个单调递增队列辅助。

代码演示

class MaxQueue {
    arr: number[];
    q: number[]
    constructor() {
        this.q = [];
        this.arr = [];
    }

    max_value(): number {
        if(!this.q.length) return -1;
        return this.q[0];
    }

    push_back(value: number): void {
        this.arr.push(value);
        while(this.q.length && this.q[this.q.length-1] < value) this.q.pop();
        this.q.push(value);
    }

    pop_front(): number {
        if(!this.arr.length) return -1;
        // 如果单调队列的队首元素与真实队列中的队首元素相同,同步弹出单调队列队首元素
        // 由于我们上面push_back的时候已经将要与加入队列元素单调性相斥的元素删除了,即
        // 所有与要添加的元素小于的元素都被删除了,而比添加元素大或相等的元素则保留下来了,因此,我们的单调队列中是包含重复元素的
        // 因此,我们发现原队列队首元素与单调队列对手元素相同时,就可以直接删除单调队列队首元素
        if(this.arr[0] === this.q[0]) this.q.shift();
        return this.arr.shift();
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * var obj = new MaxQueue()
 * var param_1 = obj.max_value()
 * obj.push_back(value)
 * var param_3 = obj.pop_front()
 */

862. 和至少为 K 的最短子数组

解题思路

依据题意,我们需要快速的计算一个区间的和值是否大于等于k,计算区间和值,我们可以使用之前聊过的前缀和数组的概念,将原数组先求出区间和数组,然后我们再使用单调队列维护可能合法的区间头指针的位置,为了确保子数组尽可能端,当我们不断往后遍历时,我们都是优先查找单调队列后面的头指针,而哪些已经弹出了单调队列的头指针,我们已经可以不需要考虑了,也就是说,我们只需要不断地往后尝试即可。

# 假设原数组
[A0,A1,A2....Ai]
# 求得原数组的前缀和数组
[S0,S1,S2....Si]
# 我们要求一个区间的和值,如S2到Si的子数组的和值
S(2~i) = Si - S2
# 假设S2是满足大于等于k的一个值,那么,我们已经找到了第一组答案了,但题目要求要是最短子数组,因此,我们还需要继续尝试能不能找到更短的。

代码演示

/*
 * @lc app=leetcode.cn id=862 lang=typescript
 *
 * [862] 和至少为 K 的最短子数组
 *
 * https://leetcode-cn.com/problems/shortest-subarray-with-sum-at-least-k/description/
 *
 * algorithms
 * Hard (18.05%)
 * Likes:    282
 * Dislikes: 0
 * Total Accepted:    14.1K
 * Total Submissions: 77.9K
 * Testcase Example:  '[1]\n1'
 *
 * 返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。
 * 
 * 如果没有和至少为 K 的非空子数组,返回 -1 。
 * 
 * 
 * 
 * 
 * 
 * 
 * 示例 1:
 * 
 * 输入:A = [1], K = 1
 * 输出:1
 * 
 * 
 * 示例 2:
 * 
 * 输入:A = [1,2], K = 4
 * 输出:-1
 * 
 * 
 * 示例 3:
 * 
 * 输入:A = [2,-1,2], K = 3
 * 输出:3
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 <= A.length <= 50000
 * -10 ^ 5 <= A[i] <= 10 ^ 5
 * 1 <= K <= 10 ^ 9
 * 
 * 
 */

// @lc code=start
function shortestSubarray(nums: number[], k: number): number {
    const sum: number[] = [0];
    const q: number[] = [];
    // 计算前缀和数组
    for(let i=0;i<nums.length;i++) sum[i+1] = sum[i] + nums[i];
    // 将前缀和数组的第一个元素的下标压入单调队列,做为初始起始点
    q.push(0);
  	// res为结果,pos为上一次找到的符合条件的数的索引,初始为-1
    let res = -1, pos = -1;
    // 循环单调和数组
    for(let i=1;i<sum.length;i++) {
        // 找到第一个满足条件的起始点位置,记录其位置并从单调队列中弹出
        while(q.length && sum[i] - sum[q[0]] >= k) {
            pos = q.shift();
        }
        // 如果找到了满足条件的数,并且结果比上一次计算的小或上一次为-1,则更新结果
        if(pos!==-1&&(res===-1||i-pos<res)) res = i - pos;
        // 将当前找到的起始点压入单调队列
        while(q.length && sum[q[q.length-1]] > sum[i]) q.pop();
        q.push(i);
    }
    return res;
};
// @lc code=end


1438. 绝对差不超过限制的最长连续子数组

解题思路

# 首先,我们来假设我们要找的符合条件的最长连续子数组就在下图的╳的阴影部分

┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┓
┃        ╳╳╳╳╳╳╳╳╳╳╳╳╳╳        ┃
┗━━━━━━━━┻━━━━━━━━━━━━┻━━━━━━━━┛
# 假设上述的阴影部分也就是我们符合条件最长连续子数组的长度为L
# 那么,大家来思考一个问题:
# 在阴影部分,L-1长度的数组是否也符合条件呢?同理L-2、L-3呢?
# 这个问题其实很简单,既然阴影部分长度为L的子数组满足条件,那么L-1、L-2、L-3自然也都满足绝对差不超过限制的条件,因为我们要求解一段区间的绝对差,首先是找到这个区间的最大值和最小值,然后相减。那么我们已经把整个阴影部分的最大值和最小值的绝对差求出来了,那么L-1、L-2、L-3的绝对差是不可能超过L长度子数组的绝对差的,因此,只要L长度的子数组满足条件,L-1、L-2、L-3长度的子数组也肯定满足条件。
# 接下来,我们再来思考一个问题:
# 在阴影部分,L+1长度的数组是否也符合条件呢?同理L+2、L+3呢?
# 显然,因为我们刚开始假设L就是满足条件的最长连续子数组的长度,那么L+1、L+2、L+3肯定就是不满足条件的了。

# 综上,我们可以总结出这样的规律
数组长度:L-1、L-2、L-3 L L+1、L+2、L+3
满足条件: √    √   √  √  ×    ×   ×
零壹模型: 1    1   1  1  0    0   0
# 从上面可以看出,我们要求的结果,其实可以看成是是之前学习二分查找算法时,经常会遇到的1-0模型求解最后一个1的问题
# 因此,我们这道题应该使用1-0模型的二分查找最后一个1的方式解决
# 那么,二分查找有一个关键的判定条件要如何确定呢?
# 我们来思考一下,我们原本的判定条件应该是当前长度L的子数组,应该要满足绝对差不超过limit。那么,要求的L是数组的长度,那么,我们是否可以把这个判定条件看作是在一个长度为L的滑动窗口中查询最大值和最小值,然后将最大值和最小值求解绝对差的问题呢?这样,我们的判定条件就可以使用两个单调队列,一个单调队列维护最大值(单调递减),一个单调队列用于维护最小值(单调递增),然后求解绝对差是否小于或等于limit即可。

# 综上所述,我们已经捋清楚了求解本题的思路了,总结一下:
# 本题需要使用二分+判定的方式求解,至于为什么要用二分+判定,详见上文。
# 而最终要的判定条件,我们可以转化为在一个长度为L的固定窗口中,求解最大值和最小值,然后求取他们的绝对差看是否小于等于limit来作为判定条件。

代码演示


/*
 * @lc app=leetcode.cn id=1438 lang=typescript
 *
 * [1438] 绝对差不超过限制的最长连续子数组
 *
 * https://leetcode-cn.com/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/description/
 *
 * algorithms
 * Medium (48.12%)
 * Likes:    199
 * Dislikes: 0
 * Total Accepted:    31K
 * Total Submissions: 64.4K
 * Testcase Example:  '[8,2,4,7]\n4'
 *
 * 给你一个整数数组 nums ,和一个表示限制的整数 limit,请你返回最长连续子数组的长度,该子数组中的任意两个元素之间的绝对差必须小于或者等于
 * limit 。
 * 
 * 如果不存在满足条件的子数组,则返回 0 。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 输入:nums = [8,2,4,7], limit = 4
 * 输出:2 
 * 解释:所有子数组如下:
 * [8] 最大绝对差 |8-8| = 0 <= 4.
 * [8,2] 最大绝对差 |8-2| = 6 > 4. 
 * [8,2,4] 最大绝对差 |8-2| = 6 > 4.
 * [8,2,4,7] 最大绝对差 |8-2| = 6 > 4.
 * [2] 最大绝对差 |2-2| = 0 <= 4.
 * [2,4] 最大绝对差 |2-4| = 2 <= 4.
 * [2,4,7] 最大绝对差 |2-7| = 5 > 4.
 * [4] 最大绝对差 |4-4| = 0 <= 4.
 * [4,7] 最大绝对差 |4-7| = 3 <= 4.
 * [7] 最大绝对差 |7-7| = 0 <= 4. 
 * 因此,满足题意的最长子数组的长度为 2 。
 * 
 * 
 * 示例 2:
 * 
 * 输入:nums = [10,1,2,4,7,2], limit = 5
 * 输出:4 
 * 解释:满足题意的最长子数组是 [2,4,7,2],其最大绝对差 |2-7| = 5 <= 5 。
 * 
 * 
 * 示例 3:
 * 
 * 输入:nums = [4,2,2,2,4,4,2,2], limit = 0
 * 输出:3
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 <= nums.length <= 10^5
 * 1 <= nums[i] <= 10^9
 * 0 <= limit <= 10^9
 * 
 * 
 */

// @lc code=start

function check(nums: number[], k:number, limit: number): boolean {
    let qMin: number[] = [], qMax:number[] = [];
    for(let i=0;i<nums.length;i++) {
        while(qMin.length && nums[qMin[qMin.length-1]] > nums[i]) qMin.pop();
        while(qMax.length && nums[qMax[qMax.length-1]] < nums[i]) qMax.pop();
        qMin.push(i);
        qMax.push(i);
        if(i + 1 < k) continue;
        if(i - qMin[0] === k) qMin.shift();
        if(i - qMax[0] === k) qMax.shift();
        if(nums[qMax[0]] - nums[qMin[0]] <= limit) return true;
    }
    return false;
    
}

function bs(nums: number[], l: number, r: number, limit: number): number {
    // 如果左右指针相遇,则说明找到了满足条件的最长数组的长度了
    if(l === r) return l;
    // 求取中间值,那么,为啥这里还要再+1呢?
    // 如果仍然使用mid = (l+r)>>1,当r-l=1时,mid = (l+r)>>1 = l,如果此时check判定通过的话
    // 就会进入l=mid的分支,因为mid=l此时,二分区间并没有缩小,会导致程序进入死循环,因此,去中间值的时候加1
    // 避免这种情况。详情可看一下《算法竞赛进阶指南》书中的 0x04 二分 章节
    let mid = (l + r + 1) >> 1;
    // 利用单调队列获取区间最大值和最小值计算绝对差是否小于或等于limit判定应该如何缩小二分区间
    if(check(nums, mid, limit)) l = mid;
    else r = mid - 1;
    return bs(nums, l, r, limit);
}


function longestSubarray(nums: number[], limit: number): number {
    return bs(nums, 0, nums.length, limit);
};
// @lc code=end


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星河阅卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值