单调队列——本质上和单调栈是一样的思路

 

算法学习笔记(66): 单调队列

“如果一个选手比你小还比你强,你就可以退役了。”——单调队列的原理

好久没写笔记了,先补一个简单的。单调队列是一种主要用于解决滑动窗口类问题的数据结构,即,在长度为 n 的序列中,求每个长度为 m 的区间的区间最值。它的时间复杂度是 O(n) ,在这个问题中比 O(nlog⁡n) 的ST表线段树要优。

单调队列的基本思想是,维护一个双向队列(deque),遍历序列,仅当一个元素可能成为某个区间最值时才保留它。

形象地打个比方,上面的序列可以看成学校里各个年级XCPC选手,数字越大代表能力越强。每个选手只能在大学四年间参赛,毕业了就没有机会了。那么,每一年的王牌选手都在哪个年级呢?

一开始的时候,大三大四的学长都比较菜,大二的最强,而大一的等大二的毕业后还有机会上位,所以队列里有两个数。

一年过去了,原本大一的成为大二,却发现新进校的新生非常强,自己再也没有机会成为最大值了,所以弹出队列。

又过了一年,新入校的新生尽管能力只有1,但理论上只要后面的人比他还菜,还是可能成为区间最大值的,所以入队。

终于,原本的王牌毕业了,后面的人以为熬出头了,谁知道这时一个巨佬级别的新生进入了集训队,这下其他所有人都没机会了。

(这只是比方,现实中各位选手的实力是会增长的,不符合这个模型ovo)

总之,观察就会发现,我们维护的这个队列总是单调递减的。如果维护区间最小值,那么维护的队列就是单调递增的。这就是为什么叫单调队列。

代码也很简洁:

deque<int> Q; // 存储的是编号
for (int i = 0; i < n; ++i)
{
    if (!Q.empty() && i - Q.front() >= m) // 毕业
        Q.pop_front();
    while (!Q.empty() && V[Q.back()] < V[i]) // 比新生弱的当场退役(求区间最小值把这里改成>即可)
        Q.pop_back();
    Q.push_back(i); // 新生入队
    if (i >= m - 1)
        cout << V[Q.front()] << " ";
}

我自己python写的代码如下:
from collections import deque


def slide_window(queue, arr, window):
    ans = [queue[0]]
    for i in range(window, len(arr)):
        while queue and queue[-1] < arr[i]:
            queue.pop()
        queue.append(arr[i])

        if arr[i-window] == queue[0]:
            queue.popleft()

        ans.append(queue[0])

    return ans


def init_queue(arr, window):
    # [1, 3, 6, 2] ==> queue: [6, 2]
    queue = deque()
    for i in range(0, window):
        while queue and queue[-1] < arr[i]:
            queue.pop()
        queue.append(arr[i])

    return queue


def find_max_window(arr, window):
    # initialize queue
    queue = init_queue(arr, window)
    # slide window
    return slide_window(queue, arr, window)


if __name__ == '__main__':
    arr = [1, 3, 6, 2, 5, 1, 7]
    print(find_max_window(arr, window=4)) # [6, 6, 6, 7]

  

重构下代码:

from collections import deque


def find_max_window(arr, window):
    ans = []
    queue = deque() # 如果是单调栈,则修改为 stack = [],后面的queue修改为stack
    for i in range(0, len(arr)):
        # 单调栈固定模板,就是一个while循环
        while queue and queue[-1] < arr[i]:
            queue.pop()

        # 满足条件后直接append元素
        queue.append(arr[i])

        # 如果是单调队列,则需要处理队头元素,如下是满足窗口过期元素,则popleft删除队头元素
        if arr[i-window] == queue[0]:
            queue.popleft()

        # 处理结果变量,根据不同的场景替换
        if i >= window - 1:
            ans.append(queue[0])

    return ans


if __name__ == '__main__':
    arr = [1, 3, 6, 2, 5, 1, 7]
    print(find_max_window(arr, window=4))  # [6, 6, 6, 7]

  

 

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

示例 2:

输入:nums = [1], k = 1
输出:[1]
from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        q = deque()
        for i in range(k):
            while q and nums[i] > q[-1]:
                q.pop()            
            q.append(nums[i])
        
        ans = [q[0]]
        for i in range(k, len(nums)):            
            while q and nums[i] > q[-1]:
                q.pop()            
            q.append(nums[i])

            if nums[i-k] == q[0]:
                q.popleft()
            
            ans.append(q[0])
        
        return ans

 

 
posted @ 2022-12-16 20:29  bonelee  阅读(48)  评论(0编辑  收藏  举报