单调栈
42. 接雨水(困难)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
方法:单调递减栈
用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。
在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到ans 。
算法
使用栈来存储条形块的索引下标。
遍历数组:
(1)当栈非空且 height[current]>height[st.top()]
意味着栈中元素可以被弹出。弹出栈顶元素 top。
计算当前元素和栈顶元素的距离,准备进行填充操作
distance=current−st.top()−1
找出界定高度
bounded_height=min(height[current],height[st.top()])−height[top]
往答案中累加积水量ans+=distance×bounded_height
(2)将当前索引下标入栈
(3)将current 移动到下个位置
即:
(1)当待入栈元素大于栈顶元素时,循环弹出栈顶元素,并计算面积;
(2)否则,入栈。
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
if n < 3:
return 0
idx, res = 0, 0
stack = []
while idx < n:
while len(stack) > 0 and height[idx] > height[stack[-1]]:
top = stack.pop()
if len(stack) == 0:
break
h = min(height[idx], height[stack[-1]]) - height[top]
dist = idx - stack[-1] -1
res += h*dist
stack.append(idx)
idx += 1
return res
84. 柱状图中最大的矩形(困难)
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
方法:单调栈
参考
https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/zhu-zhuang-tu-zhong-zui-da-de-ju-xing-by-leetcode-/
思路
例子
我们用一个具体的例子 [6, 7, 5, 2, 4, 5, 9, 3][6,7,5,2,4,5,9,3] 来帮助读者理解单调栈。我们需要求出每一根柱子的左侧且最近的小于其高度的柱子。初始时的栈为空。
我们枚举 66,因为栈为空,所以 66 左侧的柱子是「哨兵」,位置为 -1。随后我们将 66 入栈。
栈:[6(0)]。(这里括号内的数字表示柱子在原数组中的位置)
我们枚举 77,由于 6<76<7,因此不会移除栈顶元素,所以 77 左侧的柱子是 66,位置为 00。随后我们将 77 入栈。
栈:[6(0), 7(1)]
我们枚举 55,由于 7\geq 57≥5,因此移除栈顶元素 77。同样地,6 \geq 56≥5,再移除栈顶元素 66。此时栈为空,所以 55 左侧的柱子是「哨兵」,位置为 -1−1。随后我们将 55 入栈。
栈:[5(2)]
接下来的枚举过程也大同小异。我们枚举 22,移除栈顶元素 55,得到 22 左侧的柱子是「哨兵」,位置为 -1−1。将 22 入栈。
栈:[2(3)]
我们枚举 44,55 和 99,都不会移除任何栈顶元素,得到它们左侧的柱子分别是 22,44 和 55,位置分别为 33,44 和 55。将它们入栈。
栈:[2(3), 4(4), 5(5), 9(6)]
我们枚举 33,依次移除栈顶元素 99,55 和 44,得到 33 左侧的柱子是 22,位置为 33。将 33 入栈。
栈:[2(3), 3(7)]
这样以来,我们得到它们左侧的柱子编号分别为 [-1, 0, -1, -1, 3, 4, 5, 3][−1,0,−1,−1,3,4,5,3]。用相同的方法,我们从右向左进行遍历,也可以得到它们右侧的柱子编号分别为 [2, 2, 3, 8, 7, 7, 7, 8][2,2,3,8,7,7,7,8],这里我们将位置 88 看作「哨兵」。
在得到了左右两侧的柱子之后,我们就可以计算出每根柱子对应的左右边界,并求出答案了。
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
if not heights:
return 0
n = len(heights)
left, right = [0] * n, [0] * n
mono_stack = []
for i in range(n):
while mono_stack and heights[mono_stack[-1]] >= heights[i]:
mono_stack.pop()
left[i] = mono_stack[-1] if mono_stack else -1
mono_stack.append(i)
mono_stack = []
for i in range(n-1, -1, -1):
while mono_stack and heights[mono_stack[-1]] >= heights[i]:
mono_stack.pop()
right[i] = mono_stack[-1] if mono_stack else n
mono_stack.append(i)
res = max((right[i]-left[i]-1)*heights[i] for i in range(n))
return res
方法2:单调栈(方法1优化)
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
if not heights:
return 0
n = len(heights)
left, right = [0]*n, [n]*n
stack = []
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
right[stack[-1]] = i
stack.pop()
left[i] = stack[-1] if stack else -1
stack.append(i)
ans = max((right[i]-left[i]-1)*heights[i] for i in range(n))
return ans
并查集
滑动窗口(11道)
159
727
1100
340
1151
剑指 Offer 42. 连续子数组的最大和(简单)
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
方法:DP
复杂度分析:
时间复杂度 O(N): 线性遍历数组 nums 即可获得结果,使用 O(N) 时间。
空间复杂度 O(1): 使用常数大小的额外空间。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
for i in range(1, len(nums)):
nums[i] += max(nums[i-1], 0)
return max(nums)
209. 长度最小的子数组 (中等)
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3]
是该条件下的长度最小的子数组。
进阶:
如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
方法:滑动窗口
定义两个指针 start 和 end 分别表示子数组的开始位置和结束位置,维护变量sum 存储子数组中的元素和(即从nums[start] 到 nums[end] 的元素和)。
初始状态下,start 和 end 都指向下标 0,sum 的值为 0。
每一轮迭代,将nums[end] 加到 sum,如果 sum≥s,则更新子数组的最小长度(此时子数组的长度是 end−start+1),然后将 nums[start] 从 sum 中减去并将start 右移,直到sum<s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将 end 右移。
注意:数组全部和小于s的情况,返回0
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
n = len(nums)
start, sum = 0, 0
ans = n+1
for i in range(n):
sum += nums[i]
while sum >= s:
ans = min(ans, i-start+1)
sum -= nums[start]
start += 1
return 0 if ans == n+1 else ans
1004. 最大连续1的个数 III(中等)
给定一个由若干 0
和 1
组成的数组 A
,我们最多可以将 K
个值从 0 变成 1 。
返回仅包含 1 的最长(连续)子数组的长度。
示例 1:
输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
示例 2:
输入:A = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出:10
解释:
[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 10。
提示:
1 <= A.length <= 20000
0 <= K <= A.length
A[i]
为 0
或 1
方法:滑动窗口
需要记录下加入窗口的是0还是1:
如果是1,我们什么都不用做
如果是0,我们将K减1
相应地,我们需要记录移除窗口的是0还是1:
如果是1,我们什么都不做
如果是0,将K 加1
这题就是求最大的窗口。所以窗口变小是没有意义的。
1)窗口增大:left不变,right右移,即right++
什么时候增大?窗口内的0,数量没有达到上限K。
2)窗口不变:left跟着right右移,即left++,right++
什么时候不变?窗口内的0,数量达到了上限K。
class Solution:
def longestOnes(self, A: List[int], K: int) -> int:
n = len(A)
left = 0
for j in range(n):
if A[j] == 0:
K -= 1
if K < 0:
if A[left] == 0:
K += 1
left += 1
return j-left+1
1208. 尽可能使字符串相等(中等)
给你两个长度相同的字符串,s
和 t
。
将 s
中的第 i
个字符变到 t
中的第 i
个字符需要 |s[i] - t[i]|
的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost
。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s
的子字符串转化为它在 t
中对应的子字符串,则返回可以转化的最大长度。
如果 s
中没有子字符串可以转化成 t
中对应的子字符串,则返回 0
。
示例 1:
输入:s = "abcd", t = "bcdf", cost = 3
输出:3
解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
示例 2:
输入:s = "abcd", t = "cdef", cost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1
。
示例 3:
输入:s = "abcd", t = "acde", cost = 0
输出:1
解释:你无法作出任何改动,所以最大长度为 1。
提示:
1 <= s.length, t.length <= 10^5
0 <= maxCost <= 10^6
s
和 t
都只含小写英文字母。
方法:滑动窗口
对于每一对下标相等的字符,s[i]和t[i],把它们转化成相等的 cost 是已知的,
cost = abs(ord(t[i]) - ord(s[i])),
接着问题就转化为:
在一个数组中,在连续子数组的和小于等于 maxCost 的情况下,找到最长的连续子数组长度。
class Solution:
def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
n = len(s)
start, sum = 0, 0
res = 0
for i in range(n):
sum += abs(ord(s[i])-ord(t[i]))
while sum > maxCost:
sum -= abs(ord(s[start])-ord(t[start]))
start += 1
res = max(res, i-start+1)
return res
方法2:同1004
class Solution:
def equalSubstring(self, s: str, t: str, maxCost: int) -> int:
n = len(s)
start, sum = 0, 0
res = 0
for i in range(n):
sum += abs(ord(s[i]) - ord(t[i]))
if sum > maxCost:
sum -= abs(ord(s[start]) - ord(t[start]))
start += 1
# res = max(res, i-start+1)
return i-start+1
239. 滑动窗口最大值(困难)
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
示例:
输入: 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
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length
方法:双端队列
- 用一个两端开口的队列,把有可能成为滑动窗口最大值的数的数组下标存入其中。
- 待入队元素(下标)入队:
如果待入队元素比队尾元素大(大于等于),则队尾元素(下标)出队。
- 当待入队元素的下标与队首元素的下标之差 >= 滑动窗口大小时,队首元素出队。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
deque = collections.deque()
ans = []
for i, num in enumerate(nums):
if deque and i-deque[0] >= k:
deque.popleft()
while deque and nums[deque[-1]] < num:
deque.pop()
deque.append(i)
if i >= k-1:
ans.append(nums[deque[0]])
return ans