Skip to content

单调队列-上


基本介绍

应用场景

滑动窗口在滑动时,r++ 代表右侧数字窗口,l++ 代表左侧数字窗口

这个过程中,想随时得到当前滑动窗口最大值或者最小值

窗口滑动的过程中,单调队列所有调整的总代价为 O(n),单次操作的均摊代价为 O(1)

⚠️ 注意

单调队列可以和很多技巧交叉使用!比如:动态规划+单调队列优化,在【扩展】课程里讲述

模板思路

⚠️ 注意:队列中存储的是元素的下标索引,以下的思路描述是求滑动窗口最大值的情况,即从队头到队尾存储下标对应的元素大小依次从大到小排序,使用数组实现队列,优化常数时间

窗口 [ l,r )左闭右开,即 r 指向窗口末尾元素的下一个位置,并且取不到,窗口长度为 r - l

(1)最大值或者最小值总是维护在窗口的左端点,即 l 位置

(2)r++,队尾吸纳元素,只要破坏了队列中的单调性,就从队尾一直弹出元素,直到该元素能入队列并且不破坏队列中的单调性,元素相等也要弹出

(3)l++,队首元素出队,新的队头继续维护最大值或者最小值

滑动窗口的最大值

题目链接

https://leetcode.cn/problems/sliding-window-maximum/

思路分析

首先构造出 k - 1 长度的窗口,右边吸纳元素,收集答案,左边吐出元素,准备吸纳下一个元素,即准备收集第二个窗口的答案,以此类推,模拟题目的收集过程

代码实现

java
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int MAXN = 100001;
        // [l,r)左闭右开,队尾指针指向队尾元素的下一个位置
        int[] deque = new int[MAXN];
        // 队头指针
        int h = 0;
        // 队尾指针
        int t = 0;

        int n = nums.length;
        // 首先构造长度为 k-1 的窗口
        for (int i = 0; i < k - 1; i++) {
            // 单列中从左到右维护大 -> 小的单调性
            while (h < t && nums[deque[t - 1]] <= nums[i]) {
                t--;
            }
            deque[t++] = i;
        }
        int m = n - k + 1;
        int[] ans = new int[m];
        // [l,r)左闭右开,k-1 长度的窗口,r++ 收集答案,l++ 吐出元素
        for (int l = 0, r = k - 1; l < m; l++, r++) {
            // t-1 下标才是队尾元素
            while (h < t && nums[deque[t - 1]] <= nums[r]) {
                t--;
            }
            deque[t++] = r;
            // 收集答案
            ans[l] = nums[deque[h]];
            // l 位置的数出去
            if (deque[h] == l) {
                h++;
            }
            // 例如数组 [5 6 3 ...],初始 k-1 的窗口为 [6],l 指向 5,r 指向 3
            // 窗口左闭右开 [l,r),r++,3 进窗口,窗口为 [6 3],l++,5 出窗口
            // 但不能使 l++,此时 l 指向的是 6,并没有出窗口,5 在之前就没了
            // 所以 l 时候能加加,得判断 l 位置的元素和队头位置元素的值是否相等
        }
        return ans;
    }
}

绝对值 <= limit 的最长子数组

题目链接

https://leetcode.cn/problems/longest-continuous-subarray-with-absolute-diff-less-than-or-equal-to-limit/

思路分析

使用两个队列来维护当前窗口的最大值和最小值,根据如下结论,显然有单调性使用滑动窗口

根据下面的结论,窗口变小才有可能使得 max - min <= limit,如果当前窗口不符合条件,l++ 缩小窗口,此时才有可能是符合条件的,如果符合条件,继续 r++ 收集答案

⭐ 结论

对于滑动窗口,窗口变大会让 max 更大,min 更小,窗口变小会让 max 更小,min 更大

代码实现

java
class Solution {
    int MAXN = 100001;

    // 窗口内最大值的更新结构(单调队列)
    int[] maxDeque = new int[MAXN];

    // 窗口内最小值的更新结构(单调队列)
    int[] minDeque = new int[MAXN];

    // 队头与队尾指针
    int maxh = 0, maxt = 0, minh = 0, mint = 0;

    int[] arr;

    public int longestSubarray(int[] nums, int limit) {
        arr = nums;
        int n = arr.length;
        int ans = 0;
        for (int l = 0, r = 0; l < n; l++) {
            // 窗口 [l,r)左闭右开,如果当前窗口不符合条件
            // 根据结论应当缩小窗口,如果符合,继续扩大窗口
            while (r < n && ok(limit, nums[r])) {
                push(r++);
            }
            // 此时窗口 [l,r),长度为 r - l
            ans = Math.max(ans, r - l);
            // while 出来说明再扩大窗口就不符合条件了
            // 记录此时的答案,缩小窗口,判断是否符合
            pop(l);
        }
        return ans;
    }

    // 判断当前窗口是否符合 max - min <= limit
    public boolean ok(int limit, int number) {
        // number 进来,更新当前窗口的最大值和最小值
        int max = maxh < maxt ? Math.max(arr[maxDeque[maxh]], number) : number;
        int min = minh < mint ? Math.min(arr[minDeque[minh]], number) : number;
        return max - min <= limit;
    }

    // 窗口 [l,r)左闭右开,r 位置的数取不到
    public void push(int r) {
        while (maxh < maxt && arr[maxDeque[maxt - 1]] <= arr[r]) {
            maxt--;
        }
        maxDeque[maxt++] = r;
        while (minh < mint && arr[minDeque[mint - 1]] >= arr[r]) {
            mint--;
        }
        minDeque[mint++] = r;
    }

    // 吐出窗口 l 位置的数,注意判断 deque[h] == l
    public void pop(int l) {
        if (maxh < maxt && maxDeque[maxh] == l) {
            maxh++;
        }
        if (minh < mint && minDeque[minh] == l) {
            minh++;
        }
    }
}

接取落水的最小花盆

题目链接

https://www.luogu.com.cn/problem/P2698

思路分析

离散化技巧:对所有雨滴所在的位置下标排序,其他位置都是没有用的

本题问的是在一个范围内,最大与最小的差值,这个范围就是一个窗口,显然使用单调队列维护最大和最小,思路和上题类似

代码实现

java
import java.io.*;
import java.util.Arrays;

public class Main {

    public static int MAXN = 100005;

    // arr[i] -> [雨滴位置,雨滴下落时间]
    public static int[][] arr = new int[MAXN][2];

    public static int n, d;

    public static int[] maxDeque = new int[MAXN];

    public static int[] minDeque = new int[MAXN];

    public static int maxh, maxt, minh, mint;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            n = (int) in.nval;
            in.nextToken();
            d = (int) in.nval;
            for (int i = 0; i < n; i++) {
                in.nextToken();
                arr[i][0] = (int) in.nval;
                in.nextToken();
                arr[i][1] = (int) in.nval;
            }
            int ans = compute();
            out.println(ans == Integer.MAX_VALUE ? -1 : ans);
        }
        out.flush();
        out.close();
        br.close();
    }

    public static int compute() {
        // 离散化技巧,对雨滴所在位置的下标排序
        Arrays.sort(arr, 0, n, (a, b) -> a[0] - b[0]);
        // 使用静态空间,先清理脏数据
        maxh = maxt = minh = mint = 0;
        int ans = Integer.MAX_VALUE;
        // 滑动窗口
        for (int l = 0, r = 0; l < n; l++) {
            // [l,r)左闭右开,表示水滴的编号
            // l 表示花盆的左边界,arr[l][0]
            while (r < n && !ok()) {
                push(r++);
            }
            // while 结束的原因可能是不 ok 或者后面没有雨滴了
            // 所以这里要判断一下,有雨滴且符合条件时才更新答案
            if (ok()) {
                // 更新花盆的宽度
                ans = Math.min(ans, arr[r - 1][0] - arr[l][0]);
            }
            pop(l);
        }
        return ans;
    }

    public static boolean ok() {
        int max = maxh < maxt ? arr[maxDeque[maxh]][1] : 0;
        int min = minh < mint ? arr[minDeque[minh]][1] : 0;
        return max - min >= d;
    }

    public static void push(int r) {
        while (maxh < maxt && arr[maxDeque[maxt - 1]][1] <= arr[r][1]) {
            maxt--;
        }
        maxDeque[maxt++] = r;
        while (minh < mint && arr[minDeque[mint - 1]][1] >= arr[r][1]) {
            mint--;
        }
        minDeque[mint++] = r;
    }

    public static void pop(int l) {
        if (maxh < maxt && maxDeque[maxh] == l) {
            maxh++;
        }
        if (minh < mint && minDeque[minh] == l) {
            minh++;
        }
    }
}