[python刷题模板] 单调栈
一、 算法&数据结构
1. 描述
单调栈通常用来寻找数组里a[i]两侧第一个比a[i]大/小的元素。
2. 复杂度分析
- 单调栈的复杂度最坏是, O(n),但是平均来说是O(1)
- 当我们需要对数组中每个元素都搜一遍的时候,那平均每次操作就是O(1)
3. 常见应用
- 询问两边最近的小于自己的数。
4. 常用优化
- 可以初始化设置哨兵,
如:找位置i左边比它小的的节点位置,可以初始化left=[-1]*n;
找右边比它小的节点位置,初始化right=[n]*n - 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. 右边第一个更大的节点,顺序查(非逆序)
做的时候没注意,本质还是单调栈。
但是链表只能顺序查,于是还是维护了一个单调递增栈
,产生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. 最大矩形
- 对每一列dp,计算这列截止到当前行连续的1的个数,看成柱高。
- 转化为上一题剑指 Offer II 039. 直方图最大矩形面积。
- 以每行作为柱状图的底计算即可。
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
三、其他
单调栈
和单调队列
是常用极强优化,但就是不太容易想到。
四、更多例题
- 待补充