LeetCode 42.接雨水:
思路
思路1:暴力
暴力实际上也用到了双指针,分析题目中的示例,可以按行求雨水 / 按列求雨水
以按列求为例,因为按列求就是 h(雨水高度)* 宽度,而每列柱子宽度为1,因此按列求就是求 h 的累加。
而分析示例图可知,h = min(当前列左边最高柱子,当前列右边最高柱子) - 当前柱子高度。
比如说下图中蓝色箭头部分的雨水高度,左边最高柱子高度为2,右边最高柱子高度为3,当前柱子高度为0,h = min(2, 3) - 0 = 2
因此暴力求雨水就是,遍历height数组,对每列使用双指针求左边和右边最高柱子高度,再套入 h 的公式得到保存的雨水高度,h > 0 时累加入result中。需要注意的是最开始列和最后一列不保存雨水
class Solution():
def trap(self, height):
n = len(height)
result = 0
# 最开始列和最后一列不算
for i in range(1, n - 1):
maxLeft, maxRight = height[i], height[i]
# 也算双指针
for j in range(i+1, n):# 右边
maxRight = max(maxRight, height[j])
for j in range(i-1, -1, -1):左边
maxLeft = max(maxLeft, height[j])
h = max(maxRight, maxLeft) - height[i]
if h > 0: result += h
return result
因为每次遍历列的时候,还要双指针左右遍历求最高列,因此时间复杂度为O(n^2),空间复杂度为O(1),力扣上会超时
双指针优化
我们从h的公式可以看到,只要记录左边柱子的最高高度和右边柱子的最高高度,就可以计算得到当前柱子的雨水面积。
而以上暴力超时的原因在于,双指针左右遍历求最高列,这个过程中存在重复的操作。
因此我们将求左右柱子最高高度的操作独立出来,使用两个数组保存柱子的左右最高高度,
当前位置,左边的最高高度是前一个位置的左边最高高度和本柱子高度的最大值
即从左向右遍历:maxLeft[i] = max(maxLeft[i - 1], height[i])
从右向左遍历:maxRight[i] = max(maxRight[i + 1], height[i])
代码
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
if n <= 2:
return 0
# 求每列左边柱子最大高度
maxLeft = [0] * n
maxLeft[0] = height[0]
for i in range(1, n):
maxLeft[i] = max(maxLeft[i - 1], height[i])
# 求每列右边柱子最大高度
maxRight = [0] * n
maxRight[-1] = height[n - 1]
for i in range(n - 2, 0, -1):
maxRight[i] = max(maxRight[i + 1], height[i])
# 最后每列求能保存的最大雨水量 = min(左边柱子最大高度, 右边柱子最大高度) - 当前柱子高度
result = 0
for i in range(n):
h = min(maxLeft[i], maxRight[i]) - height[i] # 第一行和最后一行不会有雨水
if h > 0: result += h # 只有h>0时才加入result中
return result
单调栈
首先要明确的是,单调栈是按行求的
然后再来明确单调栈中,从栈顶到栈底元素,是从大到小还是从小到大。模拟从左到右按行求的过程,如果height[i] < top,那么存不住水,直接将其加入栈中;如果height[i] > top,那么说明出现了凹槽,此时应当计算水的容积。
volume = h * w
出现凹槽的部分在栈顶,所以出栈该元素,记为mid,那么凹槽除了底部还有左右部分,右部分为height[i],左部分为出栈后此时的栈顶元素height[stack[-1]]
h = min(height[stack[-1]], height[i]) - height[mid]
w = i - top - 1(凹槽存水的部分是中间,而且i, mid, top不一定相邻,因为是按行计算水)
综上所述,单调栈中,从栈顶到栈底为从大到小,且栈中保存的是序列。再来明晰三种情况
- height[i] < top:将 i 加入栈中
- height[i] == top,弹出栈顶元素再将 i 入栈,因为出现凹槽计算宽度时,如果遇到相同高度的柱子,需要用到最右边的柱子来计算宽度
- height[i] > top:出栈当前元素为mid,下一个栈顶元素,mid和 i 三个元素构成接水的凹槽,所以即使height[i] > top,出栈栈顶元素后还要判断栈是否为空再来计算水的容积,**再进栈 i **
代码
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
if n <= 2:
return 0
stack = [0] # 栈中保存下标
result = 0
for i in range(1, n):
if height[i] < height[stack[-1]]:
stack.append(i)
elif height[i] == height[stack[-1]]:
stack.pop()
stack.append(i)
else:
while len(stack) > 0 and height[i] > height[stack[-1]]:
mid = stack.pop()
if len(stack) > 0:
h = min(height[stack[-1]], height[i]) - height[mid]
w = i - stack[-1] - 1 # 只求中间部分
result += h * w
stack.append(i)
return result
LeetCode 84.柱状图中最大的矩形:
思路
思路1:暴力和双指针优化
本题与上题接雨水有相似之处,本题在于遍历到当前列时,求当前列构成的最大矩形,而当前列构成的最大矩形,高度为heighs[i],宽度尽可能往左右扩展。总结就是,从当前列向左向右找到**第一个高度 < heighs[i]**的列的下标,然后计算矩形面积。
# 暴力
result = 0
for i in range(n):
minLeftIndex = i - 1
while minLeftIndex >= 0 and heighs[minLeftIndex] >= heights[i]:
minLeftIndex -= 1
minRightIndex = i + 1
while minRightIndex < n and heights[minRightIndex] > heights[i]:
minRightIndex += 1
# 计算矩形体积
volume = heights[i] * (minRightIndex - minLeftIndex - 1) # 宽度只算中间部分
result = max(result, volume)
而双指针优化与上题相同,但是区别在于数组要保存的是第一个高度小于当前柱子的下标
minLeftIndex = [0] * n
minLeftIndex[0] = -1 # 避免while死循环
for i in range(1, n):
t = i - 1
while t >= 0 and heights[t] >= heights[i]:
t = minLeftIndex[t] # 注意这里
minLeftIndex[i] = t
minRightIndex = [0] * n
minRightIndex[-1] = n # 避免while死循环
for i in range(n - 2, -1, -1):
t = i + 1
while t < n and heights[t] >= heights[i]:
t = minRitghIndex[t] # 注意这里
minRightIndex[i] = t
代码
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
n = len(heights)
if n == 1:
return heights[0]
# 这里要记录左边第一个小于当前柱子高度的下标
minLeft = [0] * n
minLeft[0] = -1 # 避免while死循环
for i in range(1, n):
t = i - 1
while t >= 0 and heights[t] >= heights[i]:
t = minLeft[t]
minLeft[i] = t
# 记录右边第一个小于当前柱子高度的下标
minRight = [0] * n
minRight[-1] = n # 避免while死循环
for i in range(n - 2, -1, -1):
t = i + 1
while t < n and heights[t] >= heights[i]:
t = minRight[t]
minRight[i] = t
# 求最大体积
result = 0
for i in range(n):
volume = (minRight[i] - minLeft[i] - 1) * heights[i]
result = max(result, volume)
return result
单调栈
本题单调栈的大致思路与上题接雨水相似,但是上一题是要求左右第一根高于heights[i]的柱子,本题是要求左右第一根低于 heights[i]的柱子,所以单调栈,从栈顶到栈底是从大到小。
- 还是由栈顶元素,下一个栈顶元素和 i 组成最大面积的矩形,矩形的高为heights[i]
- 数组前后要分别添加0,因为本题中首尾柱子可以作为核心柱子
- 三种情况,heights[i] > top;heights[i] = top和heights[i] < top
代码
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
if len(heights) == 1:
return heights[0]
# 数组前后加0,与接雨水不同,最开始和最后柱子可以作为核心柱子
heights.insert(0, 0)
heights.append(0)
stack = [0]
result = 0
for i in range(1, len(heights)):
if heights[i] > heights[stack[-1]]:
stack.append(i)
elif heights[i] == heights[stack[-1]]:
stack.pop() # 可加可不加,效果相同,思路不同,暂时没想出来不加的
stack.append(i)
else:
while len(stack) > 0 and heights[i] < heights[stack[-1]]:
mid = stack.pop()
if len(stack) > 0:
volume = heights[mid] * (i - stack[-1] - 1)
result = max(result, volume)
stack.append(i)
return result
学习收获:
首先要明确题目的直观思路,即暴力/双指针思路是什么。接雨水是左右寻找最高的柱子;最大的矩形是左右寻找第一根低于当前高度的柱子。
使用单调栈来解题,明确三种情况要怎么处理,上面两道题都是下一个栈顶元素、栈顶元素和要入栈的元素组成矩形 / 接雨水,区分点在于找最高 / 第一根低于当前高度的柱子,以及首尾柱子是否能作为核心柱子(也就是上面组成矩形 / 接雨水的栈顶元素)