[python刷题模板] 单调栈

一、 算法&数据结构

1. 描述

单调栈通常用来寻找数组里a[i]两侧第一个比a[i]大/小的元素。

2. 复杂度分析

  1. 单调栈的复杂度最坏是, O(n),但是平均来说是O(1)
  2. 当我们需要对数组中每个元素都搜一遍的时候,那平均每次操作就是O(1)

3. 常见应用

  1. 询问两边最近的小于自己的数。

4. 常用优化

  1. 可以初始化设置哨兵,
    如:找位置i左边比它小的的节点位置,可以初始化left=[-1]*n;
    找右边比它小的节点位置,初始化right=[n]*n
  2. python里可以直接用list、[]
    • stack = []
    • 出栈:a = stack.pop()
    • 询问栈顶 a = stack[-1]
    • 栈里是否还有元素: if stack:
    • 入栈:stack.append()

二、 模板代码

0. 封装模板成类:寻找两边第一个比它小/大的数

例题: 907. 子数组的最小值之和
单调栈寻找两边第一个比它小的数,计算left[i]和right[i],那么i这个位置管辖的范围就是[left[i],right[i]],
于是包含i的子数组一共有多少个?:这个数组的左端点取值范围是[left[i],i],右端点取值范围是[i,right[i]],因此相乘就是个数,这些数组的最小数值都是nums[i],乘即可。
注意1:找最近的比它小的数,需要维护单调递增栈,即栈顶元素比它大的全部弹出(对于i+1来说,他们的贡献不会比i大,所以没用了)。栈顶剩下的就是最近那个。然后自己再入栈。
注意2:找右边最近的比它小的数,需要逆序遍历。但栈依然是递增的。
注意3:找比他大的,用单调递减栈。
注意4:一般栈里是存下标。


1. 寻找两边第一个比它小的数

例题: 2104. 子数组范围和

  • 这题的数据范围1000,n方能做,但是单调栈可以优化,且同时需要最大最小,做模板题很合适。
  • 计算每个值作为最大/最小值出现了几次,即出现在多少个区间里,然后显然它的贡献就是vcnt_as_max-vcnt_as_min。

  • 别看代码很长,但大部分题应该只需要调用一个方法。
class MonoStack:
    # 单调栈,计算每个数作为最大/最小值值能到的前后边界。时/空复杂度O(n)
    # 注意,这里每个方法前/后遇到相同值的情况都是相反的,
    # 如果需要真实的前后边界,需要使用get_true的方法/或者调用两个函数,然后一边取l,一边取r
    def __init__(self, a):
        self.a = a

    def get_bound_as_max_left_over_and_right_stop(self):
        """使用单调递减栈,计算
        每个值作为最大值,前后能到达的边界(寻找左右第一个比它小的值)
        这里向左会越过相同值,向右会在相同值停下来。(防止重复计算区间)
        初始值为-1/n。
        """
        a, n = self.a, len(self.a)
        l, r, st = [-1] * n, [n] * n, []
        for i, v in enumerate(a):
            while st and a[st[-1]] <= v:
                r[st.pop()] = i
            if st:
                l[i] = st[-1]
            st.append(i)
        return l, r

    def get_bound_as_max_left_stop_and_right_over(self):
        """使用单调递减栈,计算
        每个值作为最大值,前后能到达的边界(寻找左右第一个比它小的值)
        这里向左会遇到相同值停下,向右会越过相同值。(防止重复计算区间)
        初始值为-1/n。
        """
        a, n = self.a, len(self.a)
        l, r, st = [-1] * n, [n] * n, []
        for i, v in enumerate(a):
            while st and a[st[-1]] < v:
                r[st.pop()] = i
            if st:
                l[i] = st[-1]
            st.append(i)
        return l, r

    def get_true_bound_as_max(self):
        # 使用单调递减栈,计算两边的真实边界(越过相同值)
        l, _ = self.get_bound_as_max_left_over_and_right_stop()
        _, r = self.get_bound_as_max_left_stop_and_right_over()
        return l, r

    def get_bound_as_min_left_over_and_right_stop(self):
        """使用单调递增栈,计算
        每个值作为最小值,前后能到达的边界(寻找左右第一个比它大的值)
        这里向左会越过相同值,向右会在相同值停下来。(防止重复计算区间)
        初始值为-1/n。
        """
        a, n = self.a, len(self.a)
        l, r, st = [-1] * n, [n] * n, []
        for i, v in enumerate(a):
            while st and a[st[-1]] >= v:
                r[st.pop()] = i
            if st:
                l[i] = st[-1]
            st.append(i)
        return l, r

    def get_bound_as_min_left_stop_and_right_over(self):
        """使用单调递增栈,计算
        每个值作为最小值,前后能到达的边界(寻找左右第一个比它大的值)
        这里向左会遇到相同值停下,向右会越过相同值。(防止重复计算区间)
        初始值为-1/n。
        """
        a, n = self.a, len(self.a)
        l, r, st = [-1] * n, [n] * n, []
        for i, v in enumerate(a):
            while st and a[st[-1]] > v:
                r[st.pop()] = i
            if st:
                l[i] = st[-1]
            st.append(i)
        return l, r

    def get_true_bound_as_min(self):
        # 使用单调递增栈,计算两边的真实边界(越过相同值)            
        l, _ = self.get_bound_as_min_left_over_and_right_stop()
        _, r = self.get_bound_as_min_left_stop_and_right_over()
        return l, r

class Solution:
    def subArrayRanges(self, nums: List[int]) -> int:
        n = len(nums)
        ans = 0
        mst = MonoStack(nums)
        ls1,rs1 = mst.get_bound_as_max_left_over_and_right_stop()
        ls2,rs2 = mst.get_bound_as_min_left_over_and_right_stop()
       
        for i,v,l1,r1,l2,r2 in zip(range(n),nums,ls1,rs1,ls2,rs2):
            ans += (r1-i)*(i-l1)*v - (r2-i)*(i-l2)*v
        return ans

2. 寻找右边第一个比它大的元素

例题: 496. 下一个更大元素 I

单调递减栈说白了是在nums2里找每一个右边第一个比它大的元素。
题目稍微用nums1改了一下,答案只需输出nums1里边元素的结果即可。
因为nums本身是去重的,所以直接维护一个字典存每个数字的下标,最后反查即可。

class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        stack = []
        len1,len2=len(nums1),len(nums2)
        s = [0] * len2
        d = {}
        for i in range(len2-1,-1,-1):
            num = nums2[i]
            d[num] = i
            while stack and stack[-1]<=num :
                stack.pop()
            if stack:
                s[i] = stack[-1]
            else :
                s[i] = -1
            stack.append(num)
        
        return [s[d[i]] for i in nums1]

3. 右边第一个更大的节点,顺序查(非逆序)

链接: 1019. 链表中的下一个更大节点

做的时候没注意,本质还是单调栈。
但是链表只能顺序查,于是还是维护了一个单调递增栈,产生answer的时候,是产生了栈顶的ans,而不是当前节点。

class Solution:
    def nextLargerNodes(self, head: Optional[ListNode]) -> List[int]:
        answer = [0]*10005
        stack=[(0,head.val)]*10005  # pos,val
        top = 1
        head = head.next
        i = 1
        while head is not None:
            while top > 0 and head.val > stack[top-1][1]:
                answer[stack[top-1][0]] = head.val
                top -= 1
            stack[top] = (i,head.val)             
            top += 1    
            head = head.next
            i += 1
            
        return answer[:i]

4. 处理出栈数据:单调栈+dp

链接: 2289. 使数组按非递减顺序排列
大部分单调栈的应用都是处理栈中留下的数据,本题刚好相反,是处理出栈的数据。
解法想通后比较简单,具体参考
[LeetCode解题报告] 2289. 使数组按非递减顺序排列

class Solution:
    def totalSteps(self, nums: List[int]) -> int:
        """
        1.每个数只要前边有比它大的数,一定会被删除。
        2.每个数都会被他前边更大的数删除,但不一定是最近那个,因为那个数可能会被自己前边的大数删掉
          如[5,4,1,2], 2这个数不会被4删掉,因为4会被5删掉。
          但这没关系,这相当于5代替了4的位置。
        3.因此每个数被删除的时间只取决于它前边比它小的数能挡几轮:
        4.换言之:每个数被删除的时间dp[i]=max{dp[j]+1|k<j<i,k是j前边第一个比它大的数的位置},k可以用单调栈在O(n)的时间算出
        5.但这个方法是n^2的,TLE已试        .
        6.实际上步骤4中的每个j都是在单调栈构建过程中pop的那个数,那么可以在这一步同时计算dp[i]。
        7.那么是否存在dp[i]因更前方的pop而漏算的情况?
            [7,1,2,5,3,4,6],观察这个案例,5的删除次数只取决于1,2的删除次数max+1。
            而6,按步骤4的推算应该计算1,2,5,3,4每个数的次数max+1,
            实际上 dp[5] = max(dp[1,2]+1),因此不需要计算前边1,2,只需要计算5以及以后的数,即5,3,4,
            这三个数恰好是构造单调栈时,入栈6时需要pop的数。
            因此不存在漏算。
        8.额外的,每个数i若入栈前把栈删空了,说明这个数i前边没有比它更大的数,他不会被删除,dp[i] = 0
        """
        n = len(nums)
        stack = []
        dp=[0]*n
        for i in range(n):
            if i > 0 and nums[i] < nums[i-1]:
                dp[i] = 1
            while stack and nums[stack[-1]] <= nums[i]:  # 构造单调递减栈,需要把栈顶比本数小的数都干掉
                dp[i] = max(dp[i],dp[stack[-1]]+1)
                stack.pop()           
            if not stack:
                dp[i] = 0 
            stack.append(i)
        return max(dp)

5. 柱状图中的最大矩形面积,找每个柱子左右两边第一个比它矮的柱子即可(找本柱子的管辖范围)

链接: 剑指 Offer II 039. 直方图最大矩形面积

  • 转化为找左右两边第一个比它矮的柱子,那么以本柱子为高的面积只能左右扩展到这两个位置,预处理+计算都是O(n)。
class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        n = len(heights)
        stack = []
        left = [-1]*n
        right = [n]*n
        for i,h in enumerate(heights):
            while stack and heights[stack[-1]]>=h:
                stack.pop()
            if stack:
                left[i] = stack[-1]
            stack.append(i)
        stack = []
        for i in range(n-1,-1,-1):
            h = heights[i]
            while stack and heights[stack[-1]]>=h:
                stack.pop()
            if stack:
                right[i] = stack[-1]
            stack.append(i)
        
        ans = 0
        for i in range(n):
            ans = max(ans,heights[i]*(right[i]-left[i]-1))
        return ans

6. 子矩阵面积,实际是按每行找柱状图的高

链接: 85. 最大矩形

class Solution:
    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        m,n = len(matrix),len(matrix[0])
        ans = 0
        for i in range(m):
            for j in range(n):
                matrix[i][j] = int(matrix[i][j])
                if i > 0 and matrix[i][j] == 1:
                    matrix[i][j] = matrix[i-1][j] + 1
            
            left = [-1] * n
            right = [n] * n
            stack = []
            row = matrix[i]
            for j in range(n):
                while stack and row[stack[-1]] >= row[j]:
                    stack.pop()
                if stack:
                    left[j] = stack[-1]
                stack.append(j)
            stack.clear()
            for j in range(n-1,-1,-1):
                while stack and row[stack[-1]] >= row[j]:
                    stack.pop()
                if stack:
                    right[j] = stack[-1]
                stack.append(j)
            ans = max(ans ,max((right[j]-left[j]-1)*row[j] for j in range(n)))

        return ans

三、其他

  1. 单调栈单调队列是常用极强优化,但就是不太容易想到。

四、更多例题

  • 待补充

五、参考链接

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值