返回
Featured image of post 单调栈(矩形面积/贡献法/最小字典序)

单调栈(矩形面积/贡献法/最小字典序)

在这里写下你的题注

单调栈(矩形面积/贡献法/最小字典序)

LC 853 车队

853. 车队

在一条单行道上,有 n 辆车开往同一目的地。目的地是几英里以外的 target 。

给定两个整数数组 position 和 speed ,长度都是 n ,其中 position[i] 是第 i 辆车的位置, speed[i] 是第 i 辆车的速度(单位是英里/小时)。

一辆车永远不会超过前面的另一辆车,但它可以追上去,并以较慢车的速度在另一辆车旁边行驶。

车队 是指并排行驶的一辆或几辆汽车。车队的速度是车队中 最慢 的车的速度。

即便一辆车在 target 才赶上了一个车队,它们仍然会被视作是同一个车队。

返回到达目的地的车队数量 。

维护单调性

我们维护的栈是严格的,所以弹出条件就是不严格的

遍历顺序

第一个问题就是遍历顺序,从哪里遍历。在此之前,我们必须把车辆有序,这里就用到了sort函数这个好东西。我们这里按照元组(position, time_to_end)来排序,也就是按照position排序

1
pair.sort(key=lambda x:x[0], reverse=True)

第二个问题才是从哪里遍历,这里我想了半天,我们需要维护一个,从远端(更加靠近起点,距离重点远)往近端(更加靠近终点)是一种,反过来是另一种。这里面就存在两种逻辑

  • 从远到近:正序遍历,维护一个从底到顶单调递减的数组
  • 从近到远:逆序遍历,维护一个从底到顶单调递增的数组 这里我想了很久,发现是这样一个情况,两种顺序中,只有一个需要使用单调栈
  • 从远到近:正序的时候我们维护降序数组,先遍历的快车遇到后遍历的慢车会结合成车队共速。意味着总体是降序,组内是升序,小组的代表是最大的那个,代表一定出现在组内最后。所以遍历的时候,遇到先遍历快车,需要不断弹栈,然后无脑压入当前的。
  • 从近到远:逆序的时候我们维护升序数组,先遍历的慢车会拦住后遍历的慢车要求成组。意味着总体是升序,组内是降序,小组的代表是最大的那个,代表会出现在组内最前。所以遍历的时候,不可以最后无脑压栈,因为你破坏了这个性质。相反,这个只需要遍历,遇到比我大的才计数,比我小的就不管,这样反而是对的。

循环弹栈与无脑压栈

  • 循环弹栈,是因为我们要的组内代表,在遍历顺序的后面。
  • 无脑压栈,只要是比当前栈顶大,就一定碾压栈顶,在没有后继的情况下,它就会成为有效的栈顶
  • 反过来逆序遍历的时候,我们要的组内代表是先出现的,如果用单调栈,不保证任意时间停下的时候栈顶是有效元素

正序单调栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution:
    def carFleet(self, target, position, speed) -> int:
        """
        计算到达终点的车队数量。多辆车如果能在同一时间到达终点,就算作一个车队。

        参数:
        - target: 终点位置
        - position: 每辆车的初始位置列表
        - speed: 每辆车的速度列表

        返回:
        - 车队数量(最终独立到达终点的车辆组数)
        """
        
        # 将每辆车的位置和到达终点的所需时间配对
        # 公式:(目标位置 - 当前位置)/速度 = 到达时间
        pair = [(p, (target-p)/s) for p, s in zip(position, speed)]
        
        # 按位置从近到远排序(离终点远的车排前面)
        pair.sort()
        
        # 维护一个单调栈(栈顶是最快到达终点的时间)
        stk = []
        
        # 遍历每个车辆数据(从最近的到最远的)
        for _, time in pair:
            # 如果当前车的到达时间 >= 栈顶车队的到达时间,
            # 说明当前车会追上栈顶车队(成为一个车队)
            while stk and time >= stk[-1]:
                stk.pop()  # 合并车队
                
            # 将当前车的到达时间压入栈(代表一个新的潜在车队)
            stk.append(time)
        
        # 栈的大小就是独立车队数量
        return len(stk)

逆序遍历

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution:
    def carFleet(self, target, position, speed) -> int:
        """
        计算会形成多少个车队。当后车到达时间严格大于前车时,才能形成独立车队。
        采用从近到远遍历,只保留严格递增的到达时间序列。

        参数:
        - target: int, 目标位置
        - position: List[int], 每辆车的初始位置
        - speed: List[int], 每辆车的速度

        返回:
        - int, 车队数量
        """
        # 创建(位置, 到达时间)元组并按位置降序排列(近的车排在前面)
        pair = [(p, (target-p)/s) for p, s in zip(position, speed)]
        
        # 按位置从远到近排序(离终点近的车排前面)
        pair.sort(reverse=True)
        
        stk = []
        for _, time in pair:
            # 只有比栈顶更大的才会入栈
            if not stk or time > stk[-1]:
                stk.append(time)
        
        # 栈中元素个数就是独立车队数量
        return len(stk)

LC 962 最大宽度坡

这题非常好!!!我第一次理解到单调栈的魅力和意义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution:
    def maxWidthRamp(self, nums: List[int]) -> int:
        """
        计算数组中的最大宽度坡,坡定义为i <= j且nums[i] <= nums[j]
      
        参数:
        - nums: List[int],输入数组
      
        返回:
        - int,最大宽度坡的长度
        """
        stk = []
        # 遍历数组,构造单调递减栈(存储索引)
        # 栈中元素满足:nums[stk[i]] > nums[stk[i+1]] 
        for i, x in enumerate(nums):
            # 当栈为空或当前元素小于栈顶元素时才入栈
            if not stk or x < nums[stk[-1]]:
                stk.append(i)
      
        res = 0
        # 从数组末尾向前遍历
        for i in range(len(nums)-1, -1, -1):
            # 当栈不为空且当前元素大于等于栈顶元素时
            while stk and nums[i] >= nums[stk[-1]]:
                # 计算当前坡的宽度,并更新最大值
                res = max(res, i - stk.pop())
      
        return res

第一步,单调栈存左侧

这里非常重要,我们直接从nums[0]开始存一个严格递减数组的坐标,而这个坐标的意义是:所有答案坡所可能选用的左侧坐标,只有这里面的坐标可能作为左侧。

用反证法来证明,如果有一个(i, j)是答案,其中i不存在于单调栈中,则意味着在单调栈中存在k使得k < i而且nums[k] < nums[i],则(k, j)严格优于(i, j),证伪

因此这里我们直接正序遍历存栈

1
2
3
4
5
6
7
stk = []
# 遍历数组,构造单调递减栈(存储索引)
# 栈中元素满足:nums[stk[i]] > nums[stk[i+1]] 
for i, x in enumerate(nums):
	# 当栈为空或当前元素小于栈顶元素时才入栈
	if not stk or x < nums[stk[-1]]:
		stk.append(i)

第二步,逆向遍历右侧

因为右侧往左遍历时,越早遇到的 j 越能提供更大的宽度。如果当前的 nums[j] 满足 nums[j] >= nums[i]i 是栈顶左端点),那么对 i 来说,这个 j 就是它最好的匹配(最大宽度),后续更小的 j 只会缩短宽度,所以此时必须循环弹栈计算。

1
2
3
4
5
6
7
res = 0
# 从数组末尾向前遍历
for i in range(len(nums)-1, -1, -1):
	# 当栈不为空且当前元素大于等于栈顶元素时
	while stk and nums[i] >= nums[stk[-1]]:
		# 计算当前坡的宽度,并更新最大值
		res = max(res, i - stk.pop())

其他方案

看到一堆其他方案的,之后补充一下

AcWing-wzc1995大师傅


LC 2866 美丽塔 II

2866. 美丽塔 II

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Solution:
    def maximumSumOfHeights(self, maxHeights: List[int]) -> int:
        """
        计算在满足特定条件下,山高度数组的最大和。
        条件:每个位置的高度必须<=maxHeights中的对应值,且整体形成山形
        (非严格递增后非严格递减)

        参数:
            maxHeights: 输入的限制高度数组

        返回:
            满足条件的山形数组的最大和

        方法:
        1. 使用单调栈分别计算前后缀信息
        2. 前缀求左侧递增部分(非严格)
        3. 后缀求右侧递减部分(非严格)
        4. 计算每个位置作为峰值时的总最大值
        """
        n = len(maxHeights)
      
        # 计算后缀和数组:suf[i]表示从位置i开始的递减部分最大和
        suf = [0] * (n + 1)  # 额外+1位置便于处理边界情况
        stk = [n]  # 哨兵结点(虚拟的n位置)
        cur = 0     # 当前和初始化为0
        for i in range(n - 1, -1, -1):  # 从右向左遍历
            x = maxHeights[i]
            # 维护单调递增栈(保证能处理递减部分)
            while len(stk) > 1 and x <= maxHeights[stk[-1]]:
                j = stk.pop()
                cur -= (stk[-1] - j) * maxHeights[j]  # 移除弹出元素的影响
            # 添加当前元素的影响
            cur += (stk[-1] - i) * x  # 计算并累积影响范围
            suf[i] = cur  # 存储当前后缀和
            stk.append(i)  # 当前索引入栈

        # 初始化结果和前缀和计算变量
        res = 0
        pre = 0
        stk = [-1]  # 重置栈(左侧哨兵结点)

        # 计算前缀和并合并结果
        for i in range(n):  # 从左向右遍历
            x = maxHeights[i]
            # 维护单调递增栈(保证能处理递增部分)
            while len(stk) > 1 and x <= maxHeights[stk[-1]]:
                j = stk.pop()
                pre -= (j - stk[-1]) * maxHeights[j]  # 移除弹出元素的影响
            # 添加当前元素的影响
            pre += (i - stk[-1]) * x  # 计算并累积影响范围
            # 当前结果=前i个递增部分+i+1开始的递减部分
            res = max(res, pre + suf[i + 1])
            stk.append(i)  # 当前索引入栈

        return res

第一步,从后到前计算后缀

这里后缀和的目的是,维护一个单调递增的栈,栈中存的是最近的一个层级的最近(最左)的下标,这个时候就对于每个下标有这样几个操作

  • 如果当前小于等于栈顶:弹栈,把暂存的cur消除影响,当前值的影响是从当下坐标到栈顶坐标,加上自己的影响并存入后缀数组,压栈
  • 如果当前大于等于栈:添加自己影响并存入后缀数组,压栈

第二步,从前往后计算前缀

这里前缀和的目的同样,同时可以用前缀+后缀来计算最终答案


LC 84 柱状图里面的最大矩形

当时百度搜推的二面就是这道题。当时的面试官对我很不耐烦,无论是对我知识的掌握还是我的项目本身,而且,他没开摄像头。各个角度上来看,我都觉得这是一场kpi面试。但是无论如何,这是一道很重要的题。关键是,我现在想不起来我当时是怎么做的了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        """
        计算直方图中可以构成的最大矩形面积

        参数:
        - heights: 表示直方图每个柱子高度的列表

        返回:
        - int: 最大矩形面积

        算法思路:
        懒计算,遇到不合群的再一次性计算前面的,因此需要前后哨兵
        使用单调栈算法,维护一个高度单调不减的柱子索引栈
        当遇到比栈顶低的柱子时,懒计算以栈顶柱子为高的矩形面积
        """
        
        # 首尾添加高度为0的哨兵节点:
        # 开头的0保证第一个柱子能被正确计算
        # 末尾的0保证剩余栈中的柱子能被弹出计算
        heights = [0] + heights + [0]  
        stk = []  # 存储柱子索引的单调栈(保持对应高度单调不减)
        res = 0   # 最大面积结果
        
        for i, h in enumerate(heights):
            # 遇到比栈顶低的柱子时,处理所有可以被弹出的柱子
            while stk and h < heights[stk[-1]]:
                # 高度是栈顶索引所对应的高度
                hh = heights[stk.pop()]
                # 矩形实际的左侧,是栈顶下标 + 1
                # 此时的栈顶是最后一个不纳入计算面积的坐标
                ll = stk[-1] + 1
                # 矩形实际的右侧,是当前坐标 - 1
                # 因为走到了当前坐标发现不合适,当前坐标是第一个不合适的
                rr = i - 1
                res = max(res, hh * (rr - ll + 1))
            
            # 将当前柱子索引压入栈
            stk.append(i)
            
        return res

哨兵节点

哨兵节点(Sentinel Nodes)在算法的首尾各添加一个高度为 0 的柱子:

  • 头部添加 0:避免处理栈为空的边界情况,确保第一个柱子能被正确计算。
  • 尾部添加 0:确保最终所有柱子都会被处理,因为它们一定会遇到比自身小的柱子(即末尾的 0)。
1
heights = [0] + heights + [0]

矩形的计算

这里核心的是llrrhh的含义

  • hh = nums[stk.pop()]:这个是矩阵的高,它是我们弹栈的高度,我们构成整个矩阵的柱子,高度都大于等于这个,因此矩阵的最高的高,就是这个值
  • rr = i - 1:这个是矩阵的右边界,包含当前坐标。我们弹栈就是因为当前坐标i的柱子太矮了,前面的柱子都是可以计算的,所以i-1是最远的右边界
  • ll = stk[-1] + 1:这个是矩阵的左边界,包含当前坐标。我们栈顶的坐标所对应的,是最近的没有选用的柱子,有可能是没计算到这里,也可能是因为这个柱子更矮(下一次弹栈的时候会更新成这个矮的高度,大家向这个高度看齐),但是这里中间可能经历过弹栈,所以stk[-1] + 1不等于stk.pop(),中间可能经历过弹栈。所以这个是最远的左边界

单调栈的性质

单调栈存储的是柱子的 下标,并保持其对应的 高度严格单调递增

  • 当遇到比栈顶高的柱子时,直接入栈(因为它尚未找到右边界)。
  • 当遇到比栈顶低的柱子时,依次弹出栈顶较高的柱子,并计算这些柱子能形成的最大面积。

该性质保证:

  • 栈内每个柱子前面的柱子都比它
  • 如果弹出某个柱子,说明:
    • 右边第一个比它小的柱子 已经出现(heights[i])。
    • 左边第一个比它小的柱子 就是栈顶的新栈顶坐标 s[-1]

该性质某种程度上是进行了一次懒计算,等遇到不行了的小老弟,再把之前的老大哥全部弹出来进行计算。

弹栈时刻及弹栈意义

弹栈发生在 遇到一个较小的柱子 时,此时:

  1. 意义: 之前存储的高者不可继续,必须此刻结算
  2. 取出栈顶 j(当前最高的柱子)。
  3. 计算宽度
    • 右边界 i(当前柱子的索引)。
    • 左边界 stk[-1](弹出 j 后的新栈顶)。
    • 宽度 width = i - stk[-1] - 1
  4. 计算面积
    • 面积 area = heights[j] * width
  5. 更新最大值
    • max_area = max(max_area, area)

理解弹栈意义

  • 每次弹栈意味着某个柱子的右边界已经找到(此时刚遇到更小的柱子)。
  • 新栈顶 stk[-1] 是该柱子左边界(栈里存的是比当前柱子小的柱子索引)。

LC 42 接雨水

42. 接雨水

三次遍历

分别记录以每个柱子为纵向单位的时候,左边能拦住多少水,右边能拦住多少水

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution:
    def trap(self, height: List[int]) -> int:
        """
        计算柱子高度数组能接住的雨水量。

        参数:
        - height: List[int],表示每个柱子的高度。
        
        返回:
        - int,表示能接住的雨水总量。
        """
        
        n = len(height)
        
        # 初始化左右最大高度数组
        # l[i]表示第i个柱子左边(包括自身)的最大高度
        # r[i]表示第i个柱子右边(包括自身)的最大高度
        l, r = [0] * n, [0] * n
        
        # 设置边界条件
        l[0] = height[0]
        r[n-1] = height[n-1]
        
        # 从左向右遍历,填充左最大高度数组
        for i in range(1, n):
            l[i] = max(l[i-1], height[i]) 
            
        # 从右向左遍历,填充右最大高度数组
        for i in range(n-2, -1, -1):
            r[i] = max(r[i+1], height[i])
            
        res = 0  # 初始化雨水总量
        
        # 计算每个柱子的雨水贡献
        for i in range(n):
            # 每个柱子的雨水高度 = 最小左右边界高度 - 当前柱子高度
            res += min(l[i], r[i]) - height[i]
        
        return res

单调栈

维护一个柱子递减的索引栈,这里我们是当前下标和弹栈以后的栈顶构成蓄水槽

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution:
    def trap(self, height: List[int]) -> int:
        """
        使用单调栈计算柱状图可接雨水量。
        
        参数:
        - height: List[int], 表示每个柱子的高度
        
        返回:
        - int, 能接的雨水总量
        """

        res = 0  # 初始化雨水总量
        stk = []  # 单调栈,存储柱子索引,栈底到栈顶保持递减(按高度)

        for i, h in enumerate(height):  # 遍历每个柱子
            # 维护单调栈性质 - 当前柱子高于栈顶柱子时处理
            while stk and h > height[stk[-1]]:
                bottem = height[stk.pop()]  # 弹栈得到凹陷的底部高度
                
                # 如果栈不为空,说明存在左边界
                if stk:
                    # 计算当前凹槽的宽度
                    dw = i - stk[-1] - 1
                    # 计算当前凹槽的高度
                    dh = min(height[stk[-1]], h) - bottem
                    # 累计雨水量
                    res += dw * dh
            
            # 将当前柱子索引压入栈
            stk.append(i)
            
        return res

基础题单 & 进阶题单

739. 每日温度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        """
        计算每天需要等待多少天才能等到更温暖的气温
        
        参数:
        - temperatures: List[int], 每日温度列表
        
        返回:
        - List[int], 结果列表,表示每天需要等待的天数
        """
        
        # 初始化结果列表(后面会逆序存储)
        ans = []
        # 使用单调栈存储温度递减的日期索引
        stk = []
        
        # 从后往前遍历温度列表(便于计算等待天数)
        for i in range(len(temperatures)-1, -1, -1):
            # 维护单调栈:弹出所有温度小于等于当前温度的日期
            while stk and temperatures[stk[-1]] <= temperatures[i]:
                stk.pop()
            
            # 如果栈不为空,栈顶元素就是第一个比当前温度高的日期
            if stk:
                ans.append(stk[-1] - i)  # 计算等待天数
            else:
                ans.append(0)  # 没有更高温度的日期
            
            # 将当前日期压入栈中
            stk.append(i)
        
        # 因为我们是从后往前遍历的,结果需要逆序
        ans = ans[::-1]
        return ans

1475. 商品折扣后的最终价格

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution:
    def finalPrices(self, prices: List[int]) -> List[int]:
        """
        计算商品最终价格(能够使用右边第一个小于等于当前价格的折扣)
      
        参数:
        - prices: List[int], 商品原始价格列表
      
        返回:
        - List[int], 应用折扣后的最终价格列表
        """
      
        # 使用栈存储价格(维护单调递增栈)
        stk = []
        # 存储结果(后面会逆序)
        res = []
      
        # 从后往前遍历价格列表
        for i in range(len(prices)-1, -1, -1):
            # 维护单调递增栈:弹出所有比当前价格大的价格
            while stk and stk[-1] > prices[i]:
                stk.pop()
          
            # 计算最终价格:如果有更低价格就减去折扣,否则保持原价
            # 因为是从后往前遍历,stk[-1]就是右边的第一个≤当前价格的值
            res.append(prices[i] - stk[-1] if stk else prices[i])
          
            # 将当前价格压入栈中
            stk.append(prices[i])
      
        # 反转结果列表(因为是从后往前处理的)
        res = res[::-1]
        return res

496. 下一个更大元素 I

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution:
    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
        """
        在nums2中找到每个元素右侧第一个比它大的元素,并返回nums1中对应元素的结果
        
        参数:
        - nums1: List[int], 查询目标列表
        - nums2: List[int], 被查询列表
        
        返回:
        - List[int], nums1中每个元素在nums2中对应的next greater元素列表
        """
        
        # 单调栈:维护一个递减栈,用于快速找到每个元素的next greater element
        stk = []
        # 字典存储nums2中每个元素的next greater element
        nxt = {}
        
        # 从后向前遍历nums2以便找到每个元素右边第一个更大的元素
        for i in range(len(nums2)-1, -1, -1):
            # 弹出栈顶比当前元素小的元素,保证栈顶元素总是比当前元素大
            while stk and stk[-1] <= nums2[i]:
                stk.pop()
            
            # 如果栈不为空,栈顶就是当前元素的next greater element
            # 否则记录-1表示没有更大的元素
            nxt[nums2[i]] = stk[-1] if stk else -1
            
            # 将当前元素压入栈中
            stk.append(nums2[i])
        
        # 返回nums1中每个元素对应的next greater element
        return [nxt[num] for num in nums1]

503. 下一个更大元素 II

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        """
        在循环数组中找到每个元素右侧第一个比它大的元素
        
        参数:
        - nums: List[int], 循环数组
        
        返回:
        - List[int], 每个元素的下一个更大元素列表,无则返回-1
        """
        
        n = len(nums)
        stk = []  # 单调栈,维护递减序列
        res = [-1] * n  # 初始化结果为-1(表示默认没有更大的元素)
        
        # 逆序遍历两倍长度(模拟循环数组)
        for i in range(2 * n - 1, -1, -1):
            ii = i % n  # 实际索引(0到n-1之间)
            
            # 维护单调栈:弹出所有<=当前元素的栈顶元素
            while stk and stk[-1] <= nums[ii]:
                stk.pop()
            
            # 栈顶元素即为当前元素的下一个更大元素
            # 处理两遍确保循环情况下也能找到正确结果
            # 后出现的元素会覆盖前面的结果
            if stk:
                res[ii] = stk[-1]
            
            # 将当前元素压入栈中
            stk.append(nums[ii])
        
        return res

901. 股票价格跨度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class StockSpanner:
    def __init__(self):
        """
        初始化股票价格跨度计算器。
        - stk: 单调栈,保存(index, price)元组,栈中price保持递减顺序
        - 初始压入哨兵(-1, ∞),避免空栈判断
        - idx: 记录当前是第几个价格输入(从0开始计数)
        """
        self.stk = [(-1, float('inf'))]  # (index, price)栈,初始哨兵节点
        self.idx = -1  # 当前价格索引(初始为-1,next()时先自增)

    def next(self, price: int) -> int:
        """
        计算当前价格的跨度值(连续≤当前价格的天数)

        参数:
        - price: int, 当日股票价格

        返回:
        - int: 跨度值(包括当日)
        """
        self.idx += 1  # 更新当前索引
        
        # 维护单调性:弹出所有≤当前价格的条目
        while price >= self.stk[-1][1]:
            self.stk.pop()
        
        # 计算跨度 = 当前索引 - 栈顶元素的索引
        res = self.idx - self.stk[-1][0]
        
        # 将当前价格和索引进栈
        self.stk.append((self.idx, price))
        
        return res

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Solution:
    def reverseNodes(self, head):
        """
        递归反转链表
        
        参数:
        - head: ListNode,链表头节点
        
        返回:
        - ListNode,反转后的链表头节点
        """
        # 如果链表为空或只有一个节点,直接返回
        if not head or not head.next: 
            return head 
        # 递归反转后续链表
        p = self.reverseNodes(head.next)
        # 将当前节点的下一个节点指向当前节点,实现反转
        head.next.next = head 
        # 断开当前节点的原next指针,避免循环
        head.next = None 
        return p
    def nextLargerNodes(self, head: Optional[ListNode]) -> List[int]:
        """
        找出链表中每个节点后面第一个比它大的节点的值
        
        参数:
        - head: ListNode,链表头节点
        
        返回:
        - List[int],每个节点后面第一个比它大的节点的值列表
        """
        # 先将链表反转以便处理
        head = self.reverseNodes(head)
        res, stk, cur = [], [], head 
        while cur:
            # 弹出栈中所有小于等于当前节点值的元素
            while stk and stk[-1] <= cur.val:
                stk.pop()
            # 如果栈不为空,栈顶元素即为第一个比当前节点大的值
            res.append(stk[-1] if stk else 0)
            # 将当前节点值压栈
            stk.append(cur.val)
            cur = cur.next 
        # 由于是从后往前处理的,需要将结果反转
        return res[::-1]

1124. 表现良好的最长时间段

这题是这样,先用前缀和统计一下每一天的数目,劳累+1不劳累-1,这样所谓的合法区间,其实就是i < j and s[i] < s[j],这样也就变成了LC962这道题了,单调栈维护从头开始的递减的下标,逆序计算最大长度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution:
    def longestWPI(self, hours: List[int]) -> int:
        """
        计算工作日表现良好的最长区间长度(Well Performing Interval, WPI)
        该区间内"劳累天数"(>8小时)严格多于"不劳累天数"(<=8小时)
        
        参数:
        - hours: 每天工作小时数的列表
        
        返回:
        - 最长表现良好区间的天数
        """
        
        n = len(hours)
        # 前缀和数组,s[0] = 0,s[i]表示前i天的净劳累天数
        # 定义:劳累天+1,不劳累天-1
        s = [0] * (n + 1)
        
        # 单调栈,存储前缀和数组的可能左边界索引
        # 初始化为0,因为s[0]是最小的可能左边界
        stk = [0]
        
        # 第一次遍历:计算前缀和并构建单调栈
        for i, x in enumerate(hours, 1):
            # 计算前缀和
            s[i] = s[i - 1] + (1 if x > 8 else -1)
            
            # 维护单调栈(存放可能的最小左边界)
            if s[i] < s[stk[-1]]:
                stk.append(i)
        
        res = 0
        
        # 第二次遍历:从右往左检查可能的右边界
        for i in range(n, 0, -1):
            # 当前右边界i对应的前缀和s[i]是否大于某个左边界的前缀和
            while stk and s[i] > s[stk[-1]]:
                # 计算区间长度并更新结果
                res = max(res, i - stk.pop())
        
        return res

456. 132 模式

  • 从右向左遍历,每一次遍历的都是可能的one和必然的three
  • 栈中以单调不增的顺序存放three的数据
  • 如果违反单调性,此时three的栈顶成为可能的two,即坐标更大值更小
  • 我们不断弹栈,这样two会尽可能地逼近但不超过three,保证不漏过答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Solution:
    def find132pattern(self, nums: List[int]) -> bool:
        """
        检查数组中是否存在满足1-3-2模式的子序列:
        - i < j < k(下标)
        - nums[i] < nums[k] < nums[j](值)

        参数:
        - nums: 输入的整数数组

        返回:
        - 存在132模式则返回True,否则返回False
        """

        # two 存储当前可能的"2"值(即中间值)
		"""为什么two只需要一个值:
		1. 我们只需要记录在当前one右侧的、最大的满足two < one的two值
		2. 当遇到更大的one时,two会被更新为更大的值(通过弹栈)
		3. 这个过程保证了two是历史最大值中能满足nums[i] < two的最大可能值
		4. 如果有更小的two能满足条件,那一定已经在之前就被检测到了
		5. 因此只维护一个two值不会漏掉任何可能性
		"""
		two = float('-inf')

        # three 作为单调栈,存储可能的"3"候选元素
        three = []

        # 从后往前遍历数组(确定"1"的位置)
        for i in range(len(nums)-1, -1, -1):
            one = nums[i]  # 潜在的"1"值

            # 如果当前"1"值小于已找到的"2"值,说明找到了完整的132模式
            if one < two:
                return True

            # 如果当前元素比栈顶元素大,则:
            # 1. 栈顶元素一定在i右侧(因为逆向遍历)
            # 2. 栈顶元素可以成为新的潜在的"2"
            # 3. 我们把较小的元素弹出栈,因为它们不可能是更好的"3"候选
            while three and three[-1] < one:
                two = three.pop()  # 弹出栈顶作为新的"2"

            # 当前元素压入栈(成为新的"3"候选)
            three.append(one)

        # 遍历完仍未找到132模式
        return False
        

3113. 边界元素是最大值的子数组数目

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution:
    def numberOfSubarrays(self, nums: List[int]) -> int:
        """
        计算数组中满足特定条件的子数组数量
        条件:子数组的第一个元素是该子数组的最小值 

        参数:
            nums: 输入整数数组

        返回:
            满足条件的子数组总数

        方法:
        使用单调栈维护元素的频率信息
        时间复杂度: O(n),每个元素最多入栈出栈一次
        """
        
        # 初始化结果为数组长度(每个单独元素本身就是一个满足条件的子数组)
        res = len(nums)
        # 使用栈存储元组:(当前元素值, 该值作为最小值的连续出现次数)
        stk = []

        for num in nums:
            # 维护单调递增栈:
            # 弹出所有比当前元素小的元素(这些元素不可能再作为后续子数组的最小值)
            while stk and num > stk[-1][0]:
                stk.pop()
            
            # 处理与栈顶相同的元素
            if stk and stk[-1][0] == num:
                # 取出栈顶元素
                val, cnt = stk.pop()
                # 增加的子数组数量等于之前该值的连续出现次数
                res += cnt
                # 更新该值的出现次数(+1)后重新入栈
                stk.append((val, cnt + 1))
            else:
                # 新的元素入栈,初始出现次数为1
                stk.append((num, 1))
        
        return res

1944. 队列中可以看到的人数

这里我们允许相同高度的存在在栈里面,因为对于一个新的人,这些相同的人需要不同计数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution:
    def canSeePersonsCount(self, heights: List[int]) -> List[int]:
        """
        计算每个人可以看到的其他人的数量
        规则:
        - 一个人只能看到右侧比他矮的人
        - 如果右侧有更高的人,会挡住后面的视线

        参数:
            heights: 人的身高列表

        返回:
            每个人可以看到的人数列表

        方法:
        使用单调栈(单调递减)
        从右向左遍历,栈顶元素总是最近的可能阻挡视线的更高的人
        时间复杂度:O(n)
        """
        n = len(heights)
        res = [0] * n  # 初始化结果列表
        stk = []  # 使用栈来记录可能阻挡视线的人

        # 从右向左遍历
        for i in range(n - 1, -1, -1):
            # 移除栈中所有比当前人矮的人(这些人都可以被看到)
            while stk and stk[-1] < heights[i]:
                res[i] += 1  # 可以看到这个矮的人
                stk.pop()  # 移除该人(后面会被当前人阻挡)

            # 如果栈不为空,说明遇到了一个更高的或相等的人
            # 可以看到这个人(最后一个阻挡视线的人)
            if stk:
                res[i] += 1

            # 将当前人放入栈中(会成为后面人的潜在阻挡)
            stk.append(heights[i])

        return res

2454. 下一个更大元素 IV

这题很有意思,要用蓄水池来解决,两个单调栈,存坐标,左到右遍历,栈顶值最小,这里会把第一栈弹出,并且反过来塞回第二栈,意味着第二栈的单调性保证了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
```python
class Solution:
    def secondGreaterElement(self, nums: List[int]) -> List[int]:
        """
        找出每个元素的第二个更大的元素
        对于nums[i],找到nums[j]满足:
        1. j > i
        2. nums[j] > nums[i]
        3. 存在k>i使得nums[k]>nums[i]且j是第二个满足该条件的元素

        参数:
        - nums: 输入的数字列表

        返回:
        - res: 每个元素的第二个更大的元素列表,没有则返回-1

        方法说明:
        使用单调栈维护已经找到但尚未确定第二个更大元素的索引
        first栈保存还没找到第一个更大元素的索引
        second栈保存已经找到第一个但还没找到第二个更大元素的索引
        """
  
        first = []  # first保存待找第一个更大的元素
        second = [] # second保存待找第二个更大的元素
        res = [-1] * len(nums)  # 初始化结果列表,默认-1表示不存在
      
        for i in range(len(nums)):
            # 处理second栈中的元素 - 当前元素nums[i]是它们的第二个更大元素
            while second and nums[second[-1]] < nums[i]:
                res[second.pop()] = nums[i]  # 记录第二个更大的元素
          
            # 处理first栈中的元素 - 当前元素nums[i]是它们的第一个更大元素
            cur = []  # 临时保存从first栈弹出的元素(这些元素找到了第一个更大元素)
            while first and nums[first[-1]] < nums[i]:
                cur.append(first.pop())  # 这些元素找到了第一个更大的nums[i]
          
            # 将找到第一个更大元素的元素转移到second栈(保持原始顺序)
            second.extend(cur[::-1])  # 逆序添加以保证顺序正确
          
            # 当前元素进入first栈(等待找到第一个更大的元素)
            first.append(i)
          
        return res

==此处没有刷完,只是在2100 左右停下==


矩阵题单

1793. 好子数组的最大分数

LC84的变体,核心在于矩形计算的准入和准出。这题贼牛逼,我要是面试官我就出这个。这题的核心是,如何筛选出位于k两侧的左右两侧,这里就需要明确左侧和右侧是什么,这也是之前困扰了我一段时间的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution:
    def maximumScore(self, heights: List[int], k: int) -> int:
        """
        好子数组的最大分数

        参数:
        - heights: 表示直方图每个柱子高度的列表

        返回:
        - int: 最大矩形面积

        算法思路:
        和LC84的差别在于,这里要求面积必须包含k下标
        懒计算,遇到不合群的再一次性计算前面的,因此需要前后哨兵
        使用单调栈算法,维护一个高度单调不减的柱子索引栈
        当遇到比栈顶低的柱子时,懒计算以栈顶柱子为高的矩形面积
        这里注意分清楚矩形真实的左侧和右侧 ll 和 rr 到底是啥
        然后把 kk 这个约束条件加上就可以了
        """
        
        # 首尾添加高度为0的哨兵节点:
        # 开头的0保证第一个柱子能被正确计算
        # 末尾的0保证剩余栈中的柱子能被弹出计算
        heights = [0] + heights + [0]  
        stk = []  # 存储柱子索引的单调栈(保持对应高度单调不减)
        res = 0   # 最大面积结果
        kk = k + 1 # 因为哨兵产生的移位
        
        for i, h in enumerate(heights):
            # 遇到比栈顶低的柱子时,处理所有可以被弹出的柱子
            while stk and h < heights[stk[-1]]:
                # 高度是栈顶索引所对应的高度
                hh = heights[stk.pop()]
                # 矩形实际的左侧,是栈顶下标 + 1
                # 此时的栈顶是最后一个不纳入计算面积的坐标
                ll = stk[-1] + 1
                # 矩形实际的右侧,是当前坐标 - 1
                # 因为走到了当前坐标发现不合适,当前坐标是第一个不合适的
                rr = i - 1
                if ll <= kk and kk <= rr:
                    res = max(res, hh * (rr - ll + 1))
            
            # 将当前柱子索引压入栈
            stk.append(i)
            
        return res

85. 最大矩形

同样是LC84的变体,这题的核心是,我们横着扫每一行,计算在当前行中的柱子,然后套用模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Solution:
    def maxArea(self, arr):
        """
        计算直方图中的最大矩形面积。

        参数:
        - arr: List[int],表示直方图的高度数组。

        返回:
        - int,表示直方图中的最大矩形面积。
        """
        # 在数组两端添加哨兵元素0,简化边界条件处理
        arr = [0] + arr + [0]
        # 单调栈,存储柱子的索引
        stk = []
        n = len(arr)
        res = 0

        for i in range(n):
            # 当栈不为空且当前柱子的高度小于栈顶柱子的高度时
            while stk and arr[i] < arr[stk[-1]]:
                # 弹出栈顶柱子作为当前处理的高度
                hh = arr[stk.pop()]
                # 右边界为当前柱子的前一个位置
                rr = i - 1
                # 左边界为新的栈顶柱子的下一个位置
                ll = stk[-1] + 1
                # 更新最大面积
                res = max(res, hh * (rr - ll + 1))
            # 将当前柱子的索引入栈
            stk.append(i)
        return res

    def maximalRectangle(self, matrix: List[List[str]]) -> int:
        """
        计算由'1'组成的最大矩形面积。

		思路: 将问题转换为多个直方图最大矩形问题: 
		1. 从第一行开始累积每列的高度(遇到'0'则重置) 
		2. 对累积后的每一行高度数组调用maxArea方法计算当前最大矩形 
		3. 最终结果是所有行中最大的矩形面积

        参数:
        - matrix: List[List[str]],表示由'0'和'1'组成的二维矩阵。

        返回:
        - int,表示矩阵中由'1'组成的最大矩形面积。
        """
        if not matrix:
            return 0
            
        row, col = len(matrix), len(matrix[0])
        # 初始化高度数组,用于记录每一列当前连续1的高度
        arr = [0] * col 
        res = 0 

        for i in range(row):
            for j in range(col):
                # 如果是'0'则重置当前列的高度为0
                if matrix[i][j] == '0':
                    arr[j] = 0
                # 如果是'1'则增加当前列的高度
                else:
                    arr[j] += 1
            # 对每一行的高度数组计算最大矩形面积
            res = max(res, self.maxArea(arr))
        return res

1504. 统计全 1 子矩形

  • 依旧是85. 最大矩形这里面的逻辑,我们分行来计算
  • 但是核心是如何去重,答案就是使用dp来实现,其中dp[j]代表以j下标作为右下角的矩形的数量,这样一行的总和就是这一行的dp数组的加总
  • 同样我们来维护一个单调不减的栈,如果遇到更小的柱子,意味着之前的矩形需要被结算,这里我们直接不断弹栈,并找到第一个比当前柱子小的
    • 找不到,那就是左边是边界,可选的宽度从0j,这里的计数是j+1,这里的高度是arr[i]个选择
    • 找得到,那就是两种情况
      • 一种是把以栈顶为右下角的矩形全部延伸到当前节点,这个是一一对应的,所以直接相加
      • 另一种就是短板效应,纵向可选的值是arr[i],而横向可选的是从栈顶加一到当前坐标,也就是i-stk[-1]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Solution:
    def calByLine(self, arr):
        """
        计算以当前行为底边,所有全1子矩阵的数量。

        参数:
        - arr: List[int],表示当前行每列的高度(连续的1的数量)

        返回:
        - int,以当前行为底边的全1子矩阵的总数
        """
        n = len(arr)
        # dp[i] 表示以第i列为右边界的所有全1子矩阵的数量
        dp = [0] * n 
        # 单调栈,用于维护递增的高度
        stk = []
        
        for i in range(n):
            # 维护单调不减栈,弹出栈顶比当前元素大的元素
            while stk and arr[i] < arr[stk[-1]]:
                stk.pop()
            # 如果栈不为空,当前列的子矩阵数为:
            # 栈顶列的dp值 + 当前高度 × (当前列与栈顶列的间隔)
            if stk:
                dp[i] = dp[stk[-1]] + arr[i] * (i - stk[-1])
            else:
                # 栈为空时,当前列的子矩阵数为:当前高度 × (当前列索引 + 1)
                dp[i] = arr[i] * (i + 1)
            # 将当前列的索引压入栈
            stk.append(i)
        # 返回所有列的子矩阵数之和
        return sum(dp)

    def numSubmat(self, mat: List[List[int]]) -> int:
        """
        计算二进制矩阵中全1子矩阵的数量。

        参数:
        - mat: List[List[int]],输入的二进制矩阵

        返回:
        - int,全1子矩阵的总数
        """
        row, col = len(mat), len(mat[0])
        # arr 记录以当前行为底边时各列的连续1的高度
        arr = [0] * col 
        res = 0 
        
        for i in range(row):
            for j in range(col):
                # 如果当前元素是1,增加列的高度;否则重置为0
                if mat[i][j]:
                    arr[j] += 1
                else:
                    arr[j] = 0
            # 将当前行的计算结果累加到总结果中
            res += self.calByLine(arr)
        return res

贡献法题单

Licensed under CC BY-NC-SA 4.0
© 2023 - 2025 壹壹贰捌· 0Days
共书写了265.7k字·共 93篇文章 京ICP备2023035941号-1