LeetCode Go · 栈与队列(3) ​
来源与许可
题目版权归 LeetCode;题解来自 Doocs/leetcode,按 CC BY-SA 4.0 共享。补充 Go 代码来自灵茶山艾府、halfrost,保留 MIT 许可及逐题署名。本站提取 Go、调整分类及折叠排版;标注“本站整理”的 Go 解法依据题意与参考思路补写,按 CC BY-SA 4.0 共享。收录日期:2026-10-31;主题源版本:91ef8c241a5e。
本卷 17 题 · 点击题目展开,先独立思考,再查看解法。
2832. 每个元素为最大值的最大范围 🔒 ​
题目、示例与约束
现给定一个由 不同 整数构成的 0 索引数组 nums 。
我们用以下方式定义与 nums 长度相同的 0 索引数组 ans :
ans[i]是子数组nums[l..r]的 最大 长度,该子数组中的最大元素等于nums[i]。
返回数组 ans 。
注意,子数组 是数组的连续部分。
示例 1:
输入:nums = [1,5,4,3,6] 输出:[1,4,2,1,5] 解释:对于 nums[0],最长的子数组,其中最大值为 1,是 nums[0..0],所以 ans[0] = 1。 对于 nums[1],最长的子数组,是 nums[0..3],其中最大值为 5,所以 ans[1] = 4。 对于 nums[2],最长的子数组,是 nums[2..3],其中最大值为 4,所以 ans[2] = 2。 对于 nums[3],最长的子数组,是 nums[3..3],其中最大值为 3,所以 ans[3] = 1。 对于 nums[4],最长的子数组,是 nums[0..4],其中最大值为 6,所以 ans[4] = 5。
示例 2:
输入:nums = [1,2,3,4,5] 输出:[1,2,3,4,5] 解释:对于 nums[i],最长的子数组,是 nums[0..i],其中最大值与 nums[i] 相等,所以 ans[i] = i + 1。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 105- 所有
nums中的元素都是不重复的。
方法一:单调栈 · Go 实现
思考
每个位置作为区间最大值的最长范围,由左右第一个严格更大的元素决定。单调栈一次扫描即可得到两侧边界, 即为所求长度。
本题属于单调栈的模板题,我们只需要利用单调栈,求出每个元素 左边和右边第一个比它大的元素的位置,分别记为 和 ,那么 作为最大值的区间长度就是 。
时间复杂度 ,空间复杂度 。其中 为数组长度。
func maximumLengthOfRanges(nums []int) []int {
n := len(nums)
left := make([]int, n)
right := make([]int, n)
for i := range left {
left[i] = -1
right[i] = n
}
stk := []int{}
for i, x := range nums {
for len(stk) > 0 && nums[stk[len(stk)-1]] <= x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
left[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
stk = []int{}
for i := n - 1; i >= 0; i-- {
x := nums[i]
for len(stk) > 0 && nums[stk[len(stk)-1]] <= x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
right[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
ans := make([]int, n)
for i := range ans {
ans[i] = right[i] - left[i] - 1
}
return ans
}
2863. 最长半递减子数组的长度 🔒 ​
题目、示例与约束
给定一个整数数组 nums。
返回 nums 的 最长半递减子数组 的长度,如果没有这样的子数组则返回 0。
- 子数组 是数组内的连续非空元素序列。
- 一个非空数组是 半递减 的,如果它的第一个元素 严格大于 它的最后一个元素。
示例 1:
输入: nums = [7,6,5,4,3,2,1,6,10,11] 输出: 8 解释: 取子数组 [7,6,5,4,3,2,1,6]。 第一个元素是 7,最后一个元素是 6,因此满足条件。 因此,答案是子数组的长度,即 8。 可以证明,在给定条件下,没有长度大于 8 的子数组。
示例 2:
输入: nums = [57,55,50,60,61,58,63,59,64,60,63] 输出: 6 解释: 取子数组 [61,58,63,59,64,60]. 第一个元素是 61,最后一个元素是 60,因此满足条件。 因此,答案是子数组的长度,即 6。 可以证明,在给定条件下,没有长度大于 6 的子数组。
示例 3:
输入: nums = [1,2,3,4] 输出: 0 解释: 由于给定数组中没有半递减子数组,答案为 0。
提示:
1 <= nums.length <= 105-109 <= nums[i] <= 109
方法一:哈希表 + 排序 · Go 实现
思考
半递减子数组要求左端大于右端,等价于最长的逆序端点对。按值从大到小扫描,维护此前出现过的最小下标 ,当前值的最右下标与 即可构成候选长度。
题目实际上是求逆序对的最大长度,我们不妨用哈希表 记录数组中每个数字 对应的下标 。
接下来,我们按照数字从大到小的顺序遍历哈希表的键,用一个数字 维护此前出现过的最小的下标,那么对于当前的数字 ,我们可以得到一个最大的逆序对长度 ,其中 表示数组 的长度,即数字 在原数组中出现的次数,我们更新答案即可。然后,我们将 更新为 ,即数字 在原数组中第一次出现的下标。继续遍历哈希表的键,直到遍历完所有的键。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func maxSubarrayLength(nums []int) (ans int) {
d := map[int][]int{}
for i, x := range nums {
d[x] = append(d[x], i)
}
keys := []int{}
for x := range d {
keys = append(keys, x)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] > keys[j] })
k := 1 << 30
for _, x := range keys {
idx := d[x]
ans = max(ans, idx[len(idx)-1]-k+1)
k = min(k, idx[0])
}
return ans
}
2865. 美丽塔 I ​
题目、示例与约束
给定一个包含 n 个整数的数组 heights 表示 n 座连续的塔中砖块的数量。你的任务是移除一些砖块来形成一个 山脉状 的塔排列。在这种布置中,塔高度先是非递减,有一个或多个连续塔达到最大峰值,然后非递增排列。
返回满足山脉状塔排列的方案中,高度和的最大值 。
示例 1:
输入:maxHeights = [5,3,4,1,1] 输出:13 解释:我们移除一些砖块来形成 heights = [5,3,3,1,1],峰值位于下标 0。
示例 2:
输入:maxHeights = [6,5,3,9,2,7] 输出:22 解释:我们移除一些砖块来形成 heights = [3,3,3,9,2,2],峰值位于下标 3。
示例 3:
输入:maxHeights = [3,2,5,5,2,3] 输出:18 解释:我们移除一些砖块来形成 heights = [2,2,5,5,2,2],峰值位于下标 2 或 3。
提示:
1 <= n == heights.length <= 1031 <= heights[i] <= 109
方法一:枚举 · Go 实现
思考
,可枚举峰顶,再向两侧按上限递减地取高度并求和。每次扩展保持「不超过峰顶且不高于相邻外侧」的单峰约束。
我们可以枚举每一座塔作为最高塔,每一次向左右两边扩展,算出其他每个位置的高度,然后累加得到高度和 。求出所有高度和的最大值即可。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func maximumSumOfHeights(maxHeights []int) (ans int64) {
n := len(maxHeights)
for i, x := range maxHeights {
y, t := x, x
for j := i - 1; j >= 0; j-- {
y = min(y, maxHeights[j])
t += y
}
y = x
for j := i + 1; j < n; j++ {
y = min(y, maxHeights[j])
t += y
}
ans = max(ans, int64(t))
}
return
}
方法二:动态规划 + 单调栈 · Go 实现
思考
方法一每次从峰顶线性扩展,总时间 ,在更大 下不足。单调栈求出每座塔左侧(右侧)最近的更矮位置,前缀美丽值 便可在常数时间内由 递推;左右各算一遍后在峰顶处拼接。
方法一的做法足以通过本题,但是时间复杂度较高。我们可以使用“动态规划 + 单调栈”来优化枚举的过程。
我们定义 表示前 座塔中,以最后一座塔作为最高塔的美丽塔方案的高度和。我们可以得到如下的状态转移方程:
其中 是最后一座塔左边第一个高度小于等于 的塔的下标。我们可以使用单调栈来维护这个下标。
我们可以使用类似的方法求出 ,表示从右往左,以第 座塔作为最高塔的美丽塔方案的高度和。最终答案即为 的最大值。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func maximumSumOfHeights(maxHeights []int) (ans int64) {
n := len(maxHeights)
stk := []int{}
left := make([]int, n)
right := make([]int, n)
for i := range left {
left[i] = -1
right[i] = n
}
for i, x := range maxHeights {
for len(stk) > 0 && maxHeights[stk[len(stk)-1]] > x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
left[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
stk = []int{}
for i := n - 1; i >= 0; i-- {
x := maxHeights[i]
for len(stk) > 0 && maxHeights[stk[len(stk)-1]] >= x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
right[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
f := make([]int64, n)
g := make([]int64, n)
for i, x := range maxHeights {
if i > 0 && x >= maxHeights[i-1] {
f[i] = f[i-1] + int64(x)
} else {
j := left[i]
f[i] = int64(x) * int64(i-j)
if j != -1 {
f[i] += f[j]
}
}
}
for i := n - 1; i >= 0; i-- {
x := maxHeights[i]
if i < n-1 && x >= maxHeights[i+1] {
g[i] = g[i+1] + int64(x)
} else {
j := right[i]
g[i] = int64(x) * int64(j-i)
if j != n {
g[i] += g[j]
}
}
}
for i, x := range maxHeights {
ans = max(ans, f[i]+g[i]-int64(x))
}
return
}
2866. 美丽塔 II ​
题目、示例与约束
给你一个长度为 n 下标从 0 开始的整数数组 maxHeights 。
你的任务是在坐标轴上建 n 座塔。第 i 座塔的下标为 i ,高度为 heights[i] 。
如果以下条件满足,我们称这些塔是 美丽 的:
1 <= heights[i] <= maxHeights[i]heights是一个 山脉 数组。
如果存在下标 i 满足以下条件,那么我们称数组 heights 是一个 山脉 数组:
- 对于所有
0 < j <= i,都有heights[j - 1] <= heights[j] - 对于所有
i <= k < n - 1,都有heights[k + 1] <= heights[k]
请你返回满足 美丽塔 要求的方案中,高度和的最大值 。
示例 1:
输入:maxHeights = [5,3,4,1,1] 输出:13 解释:和最大的美丽塔方案为 heights = [5,3,3,1,1] ,这是一个美丽塔方案,因为: - 1 <= heights[i] <= maxHeights[i] - heights 是个山脉数组,峰值在 i = 0 处。 13 是所有美丽塔方案中的最大高度和。
示例 2:
输入:maxHeights = [6,5,3,9,2,7] 输出:22 解释: 和最大的美丽塔方案为 heights = [3,3,3,9,2,2] ,这是一个美丽塔方案,因为: - 1 <= heights[i] <= maxHeights[i] - heights 是个山脉数组,峰值在 i = 3 处。 22 是所有美丽塔方案中的最大高度和。
示例 3:
输入:maxHeights = [3,2,5,5,2,3] 输出:18 解释:和最大的美丽塔方案为 heights = [2,2,5,5,2,2] ,这是一个美丽塔方案,因为: - 1 <= heights[i] <= maxHeights[i] - heights 是个山脉数组,最大值在 i = 2 处。 注意,在这个方案中,i = 3 也是一个峰值。 18 是所有美丽塔方案中的最大高度和。
提示:
1 <= n == maxHeights.length <= 1051 <= maxHeights[i] <= 109
方法一:动态规划 + 单调栈 · Go 实现
思考
本题 更大,不能再从每个峰顶线性扩展。与上一题的优化相同:单调栈得到左右更矮边界,动态规划分别计算以 为右端(左端)峰顶的高度和,再在 处相加并减去重复的 。
我们定义 表示前 座塔中,以最后一座塔作为最高塔的美丽塔方案的高度和。我们可以得到如下的状态转移方程:
其中 是最后一座塔左边第一个高度小于等于 的塔的下标。我们可以使用单调栈来维护这个下标。
我们可以使用类似的方法求出 ,表示从右往左,以第 座塔作为最高塔的美丽塔方案的高度和。最终答案即为 的最大值。
时间复杂度 ,空间复杂度 。其中 为数组 的长度。
func maximumSumOfHeights(maxHeights []int) (ans int64) {
n := len(maxHeights)
stk := []int{}
left := make([]int, n)
right := make([]int, n)
for i := range left {
left[i] = -1
right[i] = n
}
for i, x := range maxHeights {
for len(stk) > 0 && maxHeights[stk[len(stk)-1]] > x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
left[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
stk = []int{}
for i := n - 1; i >= 0; i-- {
x := maxHeights[i]
for len(stk) > 0 && maxHeights[stk[len(stk)-1]] >= x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
right[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
f := make([]int64, n)
g := make([]int64, n)
for i, x := range maxHeights {
if i > 0 && x >= maxHeights[i-1] {
f[i] = f[i-1] + int64(x)
} else {
j := left[i]
f[i] = int64(x) * int64(i-j)
if j != -1 {
f[i] += f[j]
}
}
}
for i := n - 1; i >= 0; i-- {
x := maxHeights[i]
if i < n-1 && x >= maxHeights[i+1] {
g[i] = g[i+1] + int64(x)
} else {
j := right[i]
g[i] = int64(x) * int64(j-i)
if j != n {
g[i] += g[j]
}
}
}
for i, x := range maxHeights {
ans = max(ans, f[i]+g[i]-int64(x))
}
return
}
3174. 清除数字 ​
题目、示例与约束
给你一个字符串 s 。
你的任务是重复以下操作删除 所有 数字字符:
- 删除 第一个数字字符 以及它左边 最近 的 非数字 字符。
请你返回删除所有数字字符以后剩下的字符串。
注意,该操作不能对左侧没有任何非数字字符的数字执行。
示例 1:
输入:s = "abc"
输出:"abc"
解释:
字符串中没有数字。
示例 2:
输入:s = "cb34"
输出:""
解释:
一开始,我们对 s[2] 执行操作,s 变为 "c4" 。
然后对 s[1] 执行操作,s 变为 "" 。
提示:
1 <= s.length <= 100s只包含小写英文字母和数字字符。- 输入保证所有数字都可以按以上操作被删除。
方法一:栈 + 模拟 · Go 实现
思考
每遇到数字就删掉它和左侧最近的非数字。反复在字符串上
remove是平方的。从左扫描时,栈顶恰是「当前左侧最近仍保留的字符」,遇数字弹栈即模拟一次删除。
字母入栈、数字弹栈,最后把栈拼回字符串。每个字符进出一次。
我们用一个栈 stk 来模拟这个过程,遍历字符串 s,如果当前字符是数字,就弹出栈顶元素,否则将当前字符入栈。
最后将栈中的元素拼接成字符串返回。
时间复杂度 ,空间复杂度 。其中 为字符串 s 的长度。
func clearDigits(s string) string {
stk := []byte{}
for i := range s {
if s[i] >= '0' && s[i] <= '9' {
stk = stk[:len(stk)-1]
} else {
stk = append(stk, s[i])
}
}
return string(stk)
}
3359. 查找最大元素不超过 K 的有序子矩阵 🔒 ​
题目、示例与约束
给定一个大小为 m x n 的二维矩阵 grid。同时给定一个 非负整数 k。
返回满足下列条件的 grid 的子矩阵数量:
- 子矩阵中最大的元素 小于等于
k。 - 子矩阵的每一行都以 非递增 顺序排序。
矩阵的子矩阵 (x1, y1, x2, y2) 是通过选择所有满足 x1 <= x <= x2 且 y1 <= y <= y2 的 grid[x][y] 元素组成的矩阵。
示例 1:
输入:grid = [[4,3,2,1],[8,7,6,1]], k = 3
输出:8
解释:

8 个子矩阵分别是:
[[1]][[1]][[2,1]][[3,2,1]][[1],[1]][[2]][[3]][[3,2]]
示例 2:
输入:grid = [[1,1,1],[1,1,1],[1,1,1]], k = 1
输出:36
解释:
矩阵中有 36 个子矩阵。所有子矩阵的最大元素都等于 1。
示例 3:
输入:grid = [[1]], k = 1
输出:1
提示:
1 <= m == grid.length <= 1031 <= n == grid[i].length <= 1031 <= grid[i][j] <= 1091 <= k <= 109
方法一:Go 解法(本站整理) · Go 实现
根据题意整理:源文没有非空实现,其思考把行条件写成了列条件,此处按题面“每一行非递增”处理。对每个右边界列,维护每行向左连续合法的宽度 w:当前值超过 k 时清零,向左相邻值≥当前值且前段合法时延长,否则从 1 开始。固定上下边界的矩形数量就是这些行的最小 w;沿行扫描用单调栈维护所有以当前行为底的连续段的最小值之和。每个元素至多入栈出栈一次。m、n 为矩阵尺寸,时间 O(mn),额外空间 O(m),返回 int64 防止百万格矩阵计数溢出。入口 countSubmatrices。
出处:Doocs 原题与题解,固定提交 91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011;题意、参考及本 Go 改写采用 CC BY-SA 4.0。
// 题意与参考:Doocs LeetCode,CC BY-SA 4.0。
// https://github.com/doocs/leetcode/blob/91ef8c241a5eaabf96f9ca8d45cfb1a338bcb011/solution/3300-3399/3359.Find%20Sorted%20Submatrices%20With%20Maximum%20Element%20at%20Most%20K/README.md
// Go 实现与测试沿用 CC BY-SA 4.0;来源性质见配套说明。
func countSubmatrices(grid [][]int, k int) int64 {
m, n := len(grid), len(grid[0])
width := make([]int, m)
type entry struct{ width, rows int }
stack := make([]entry, 0, m)
var ans int64
for col := 0; col < n; col++ {
stack = stack[:0]
var ending int64
for row := 0; row < m; row++ {
if grid[row][col] > k {
width[row] = 0
} else if col > 0 && grid[row][col-1] >= grid[row][col] {
width[row]++
} else {
width[row] = 1
}
w := width[row]
count := 1
for len(stack) > 0 && stack[len(stack)-1].width >= w {
last := stack[len(stack)-1]
stack = stack[:len(stack)-1]
ending -= int64(last.width) * int64(last.rows)
count += last.rows
}
stack = append(stack, entry{w, count})
ending += int64(w) * int64(count)
ans += ending
}
}
return ans
}
3412. 计算字符串的镜像分数 ​
题目、示例与约束
给你一个字符串 s。
英文字母中每个字母的 镜像 定义为反转字母表之后对应位置上的字母。例如,'a' 的镜像是 'z','y' 的镜像是 'b'。
最初,字符串 s 中的所有字符都 未标记 。
字符串 s 的初始分数为 0 ,你需要对其执行以下过程:
- 从左到右遍历字符串。
- 对于每个下标
i,找到距离最近的 未标记 下标j,下标j需要满足j < i且s[j]是s[i]的镜像。然后 标记 下标i和j,总分加上i - j的值。 - 如果对于下标
i,不存在满足条件的下标j,则跳过该下标,继续处理下一个下标,不需要进行标记。
返回最终的总分。
示例 1:
输入: s = "aczzx"
输出: 5
解释:
i = 0。没有符合条件的下标j,跳过。i = 1。没有符合条件的下标j,跳过。i = 2。距离最近的符合条件的下标是j = 0,因此标记下标 0 和 2,然后将总分加上2 - 0 = 2。i = 3。没有符合条件的下标j,跳过。i = 4。距离最近的符合条件的下标是j = 1,因此标记下标 1 和 4,然后将总分加上4 - 1 = 3。
示例 2:
输入: s = "abcdef"
输出: 0
解释:
对于每个下标 i,都不存在满足条件的下标 j。
提示:
1 <= s.length <= 105s仅由小写英文字母组成。
方法一:哈希表 · Go 实现
思考
每个尚未配对的字符要与其左侧最近的、未标记的镜像字符配对,得分为下标差。 ,若每次都向左扫描,最坏平方。
镜像关系是对合: 的镜像为 当且仅当 的镜像为 。配对总是取「最近的未使用镜像」,这正是按字符分栈时的栈顶。
用哈希表把每种字符尚未配对的下标压栈。扫到 时若镜像 的栈非空,则弹出最近的 并加上 ;否则把 压入 的栈。
我们可以用一个哈希表 来存储每个未标记的字符的下标列表,其中键是字符,值是下标列表。
我们遍历字符串 ,对于每个字符 ,我们找到其镜像字符 ,如果 中存在 ,我们就取出 对应的下标列表 ,取出 的最后一个元素 ,并将 从 中移除。如果 变为空,我们就将 从 中移除。此时,我们就找到了一个满足条件的下标对 ,并将 加到答案中。否则,我们将 加入到 中。
时间复杂度 ,空间复杂度 。其中 为字符串 的长度。
func calculateScore(s string) (ans int64) {
d := make(map[rune][]int)
for i, x := range s {
y := 'a' + 'z' - x
if ls, ok := d[y]; ok {
j := ls[len(ls)-1]
d[y] = ls[:len(ls)-1]
if len(d[y]) == 0 {
delete(d, y)
}
ans += int64(i - j)
} else {
d[x] = append(d[x], i)
}
}
return
}
3430. 最多 K 个元素的子数组的最值之和 ​
题目、示例与约束
给你一个整数数组 nums 和一个 正 整数 k 。 返回 最多 有 k 个元素的所有子数组的 最大 和 最小 元素之和。
示例 1:
输入:nums = [1,2,3], k = 2
输出:20
解释:
最多 2 个元素的 nums 的子数组:
| 子数组 | 最小 | 最大 | 和 |
|---|---|---|---|
[1] |
1 | 1 | 2 |
[2] |
2 | 2 | 4 |
[3] |
3 | 3 | 6 |
[1, 2] |
1 | 2 | 3 |
[2, 3] |
2 | 3 | 5 |
| 总和 | 20 |
输出为 20 。
示例 2:
输入:nums = [1,-3,1], k = 2
输出:-6
解释:
最多 2 个元素的 nums 的子数组:
| 子数组 | 最小 | 最大 | 和 |
|---|---|---|---|
[1] |
1 | 1 | 2 |
[-3] |
-3 | -3 | -6 |
[1] |
1 | 1 | 2 |
[1, -3] |
-3 | 1 | -2 |
[-3, 1] |
-3 | 1 | -2 |
| 总和 | -6 |
输出为 -6 。
提示:
1 <= nums.length <= 800001 <= k <= nums.length-106 <= nums[i] <= 106
方法一:单调栈(贡献计数) · Go 实现
思考
与子序列版不同,这里统计的是长度不超过 的连续子数组的最大、最小值之和。 较大时,每个子数组重算最值不可行。
以右端 为界,所有合法左端上的最大(最小)值,会在单调栈弹出时把「作为最值的次数」转移给新元素。
维护带 的单调栈,并在窗口左端滑过 时扣掉过期贡献。每个 的 相加即答案。
本题要求计算所有长度不超过 的子数组中:
- 子数组最大值之和
- 子数组最小值之和
并返回两者之和。
我们定义:
表示所有以索引 结尾的合法子数组,它们各自最大值的总和。
同理:
表示所有以索引 结尾的合法子数组,它们各自最小值的总和。
接下来,我们维护两个单调队列:
max_stack:维护当前窗口内所有子数组可能的最大值。min_stack:维护当前窗口内所有子数组可能的最小值。
虽然底层数据结构为 deque,但由于绝大多数操作都发生在右侧,因此其本质仍然是单调栈。
队列中的每个元素格式为:
(index, value, shares)
其中:
index:元素索引value:元素值shares:该元素在当前窗口内,作为子数组最大值 / 最小值的贡献次数
func minMaxSubarraySum(nums []int, k int) int64 {
var total, windowMax, windowMin int64
type item struct {
idx, num int
shares int64
}
maxStack, minStack := []item{}, []item{}
for end := 0; end < len(nums); end++ {
start := end - k + 1
if start < 0 {
start = 0
}
if start > 0 {
maxStack[0].shares--
windowMax -= int64(maxStack[0].num)
if maxStack[0].idx < start {
maxStack = maxStack[1:]
}
minStack[0].shares--
windowMin -= int64(minStack[0].num)
if minStack[0].idx < start {
minStack = minStack[1:]
}
}
num := int64(nums[end])
maxShares := int64(1)
windowMax += num
for len(maxStack) > 0 && int64(maxStack[len(maxStack)-1].num) <= num {
prev := maxStack[len(maxStack)-1]
maxStack = maxStack[:len(maxStack)-1]
maxShares += prev.shares
windowMax += (num - int64(prev.num)) * prev.shares
}
maxStack = append(maxStack, item{end, nums[end], maxShares})
minShares := int64(1)
windowMin += num
for len(minStack) > 0 && int64(minStack[len(minStack)-1].num) >= num {
prev := minStack[len(minStack)-1]
minStack = minStack[:len(minStack)-1]
minShares += prev.shares
windowMin += (num - int64(prev.num)) * prev.shares
}
minStack = append(minStack, item{end, nums[end], minShares})
total += windowMax + windowMin
}
return total
}
3561. 移除相邻字符 ​
题目、示例与约束
给你一个由小写英文字母组成的字符串 s。
你 必须 在字符串 s 中至少存在两个 连续 字符时,反复执行以下操作:
- 移除字符串中 最左边 的一对按照字母表 连续 的相邻字符(无论是按顺序还是逆序,例如
'a'和'b',或'b'和'a')。 - 将剩余字符向左移动以填补空隙。
当无法再执行任何操作时,返回最终的字符串。
注意:字母表是循环的,因此 'a' 和 'z' 也视为连续。
示例 1:
输入: s = "abc"
输出: "c"
解释:
- 从字符串中移除
"ab",剩下"c"。 - 无法进行进一步操作。因此,所有可能移除操作后的最终字符串为
"c"。
示例 2:
输入: s = "adcb"
输出: ""
解释:
- 从字符串中移除
"dc",剩下"ab"。 - 从字符串中移除
"ab",剩下""。 - 无法进行进一步操作。因此,所有可能移除操作后的最终字符串为
""。
示例 3:
输入: s = "zadb"
输出: "db"
解释:
- 从字符串中移除
"za",剩下"db"。 - 无法进行进一步操作。因此,所有可能移除操作后的最终字符串为
"db"。
提示:
1 <= s.length <= 105s仅由小写英文字母组成。
方法一:栈 · Go 实现
思考
相邻且在字母表中连续(含
a/z)的字符成对消去,类似括号匹配,可能引发连锁。栈恰好表达「当前仍未消去的后缀」。扫每个字符:若与栈顶距离为 或 则弹出,否则入栈。最终栈中序列即无法再消的结果。
我们可以使用栈来模拟移除相邻字符的过程。遍历字符串中的每个字符,如果栈顶字符与当前字符是连续的(即它们的 ASCII 值差为 1 或 25),则将栈顶字符弹出;否则,将当前字符压入栈中。最后,栈中的字符就是无法再移除的结果,我们将栈中的字符连接成字符串并返回。
时间复杂度 ,空间复杂度 ,其中 是字符串的长度。
func resultingString(s string) string {
isContiguous := func(a, b rune) bool {
x := abs(int(a - b))
return x == 1 || x == 25
}
stk := []rune{}
for _, c := range s {
if len(stk) > 0 && isContiguous(stk[len(stk)-1], c) {
stk = stk[:len(stk)-1]
} else {
stk = append(stk, c)
}
}
return string(stk)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3676. 碗子数组的数目 ​
题目、示例与约束
给你一个整数数组 nums,包含 互不相同 的元素。
nums 的一个子数组 nums[l...r] 被称为 碗(bowl),如果它满足以下条件:
- 子数组的长度至少为 3。也就是说,
r - l + 1 >= 3。 - 其两端元素的 最小值 严格大于 中间所有元素的 最大值。也就是说,
min(nums[l], nums[r]) > max(nums[l + 1], ..., nums[r - 1])。
返回 nums 中 碗 子数组的数量。
示例 1:
输入: nums = [2,5,3,1,4]
输出: 2
解释:
碗子数组是 [3, 1, 4] 和 [5, 3, 1, 4]。
[3, 1, 4]是一个碗,因为min(3, 4) = 3 > max(1) = 1。[5, 3, 1, 4]是一个碗,因为min(5, 4) = 4 > max(3, 1) = 3。
示例 2:
输入: nums = [5,1,2,3,4]
输出: 3
解释:
碗子数组是 [5, 1, 2]、[5, 1, 2, 3] 和 [5, 1, 2, 3, 4]。
示例 3:
输入: nums = [1000000000,999999999,999999998]
输出: 0
解释:
没有子数组是碗。
提示:
3 <= nums.length <= 1051 <= nums[i] <= 109nums由不同的元素组成。
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
从易到难,先思考原问题的一个简单版本:
- 只统计满足 的合法子数组的数量。相当于这个碗是「左高右低」的。
由于中间的数都比 小,从形状( 的折线图)上看, 是 左侧最近的大于等于 的数。
为什么是最近?左端点还能再往左吗?
不能。如果继续向左,让子数组包含更多元素,那么 就会变成子数组的中间元素。题目要求中间元素必须小于两端元素,但 ,不符合题目要求。
所以对于每个右端点 ,我们只需要找 左侧最近的大于等于 的数的下标 。如果 存在且 ,那么找到了一个合法子数组,把答案加一。
这是单调栈的标准应用,请看 单调栈【基础算法精讲 26】。
对于 的情况,同样可以用单调栈计算。
⚠注意:小心子数组两端点元素相同的情况,不能重复统计。不妨规定,左侧找大于等于,右侧找严格大于。虽然本题保证「所有元素互不相同」,但我的做法也适用于有重复元素的情况。
此外,上述结论表明,答案的上界是 (粗略估计),所以返回值用 就够了。本题没有重复元素,答案的上界是 (见写法二)。
本题视频讲解,欢迎点赞关注~
写法一
写法二
注意到,无论是左大右小(比如 )还是左小右大(比如 ),当我们遍历到 时,如果出栈后栈不为空,说明栈顶和 是合法子数组的左右端点。「栈不为空」这个条件还说明子数组的长度至少是 。
比如 ,遍历到 时:
- 弹出 ,发现栈不为空,那么 是符合要求的子数组。
- 弹出 ,发现栈不为空,那么 是符合要求的子数组。
- 弹出 ,发现栈不为空,那么 是符合要求的子数组。
注意本题保证所有元素互不相同。
复杂度分析
- 时间复杂度:,其中 是 的长度。虽然写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此整个二重循环的总循环次数为 。
- 空间复杂度:。
空间优化
遍历过的数不会再用到,可以充分利用遍历过的空间,把 当作栈。
复杂度分析
- 时间复杂度:,其中 是 的长度。虽然写了个二重循环,但站在每个元素的视角看,这个元素在二重循环中最多入栈出栈各一次,因此整个二重循环的总循环次数为 。
- 空间复杂度:。
思考题
本题要求子数组两个端点都大于中间元素,如果改成至少有一个端点大于中间元素,怎么做?
欢迎在评论区分享你的思路/代码。
专题训练
见下面单调栈题单的第一章。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
// https://space.bilibili.com/206214
func bowlSubarrays1(nums []int) (ans int64) {
st := []int{}
for i, x := range nums {
for len(st) > 0 && nums[st[len(st)-1]] < x {
// j=st[len(st)-1] 右侧严格大于 nums[j] 的数的下标是 i
if i-st[len(st)-1] > 1 { // 子数组的长度至少为 3
ans++
}
st = st[:len(st)-1]
}
// i 左侧大于等于 nums[i] 的数的下标是 st[len(st)-1]
if len(st) > 0 && i-st[len(st)-1] > 1 { // 子数组的长度至少为 3
ans++
}
st = append(st, i)
}
return
}
func bowlSubarrays(nums []int) (ans int64) {
st := nums[:0]
for _, x := range nums {
for len(st) > 0 && st[len(st)-1] < x {
st = st[:len(st)-1]
if len(st) > 0 {
ans++
}
}
st = append(st, x)
}
return
}
3703. 移除K-平衡子字符串 ​
题目、示例与约束
给你一个只包含 '(' 和 ')' 的字符串 s,以及一个整数 k。
如果一个 字符串 恰好是 k 个 连续 的 '(' 后面跟着 k 个 连续 的 ')',即 '(' * k + ')' * k ,那么称它是 k-平衡 的。
例如,如果 k = 3,k-平衡字符串是 "((()))"。
你必须 重复地 从 s 中移除所有 不重叠 的 k-平衡子串,然后将剩余部分连接起来。持续这个过程直到不存在 k-平衡 子串 为止。
返回所有可能的移除操作后的最终字符串。
子串 是字符串中 连续 的 非空 字符序列。
示例 1:
输入: s = "(())", k = 1
输出: ""
解释:
k-平衡子串是 "()"
| 步骤 | 当前 s |
k-平衡 |
结果 s |
|---|---|---|---|
| 1 | (()) |
( |
() |
| 2 | () |
() |
Empty |
因此,最终字符串是 ""。
示例 2:
输入: s = "(()(", k = 1
输出: "(("
解释:
k-平衡子串是 "()"
| 步骤 | 当前 s |
k-平衡 |
结果 s |
|---|---|---|---|
| 1 | (()( |
( |
(( |
| 2 | (( |
- | (( |
因此,最终字符串是 "(("。
示例 3:
输入: s = "((()))()()()", k = 3
输出: "()()()"
解释:
k-平衡子串是 "((()))"
| 步骤 | 当前 s |
k-平衡 |
结果 s |
|---|---|---|---|
| 1 | ((()))()()() |
|
()()() |
| 2 | ()()() |
- | ()()() |
因此,最终字符串是 "()()()"。
提示:
2 <= s.length <= 105s仅由'('和')'组成。1 <= k <= s.length / 2
方法一:栈 · Go 实现
思考
反复在字符串中查找并删除 段连续括号,朴素替换会多次扫描同一段。连续相同字符可以压缩,栈中只存「字符及其个数」。每当栈顶恰好积累 个右括号、且其下至少还有 个左括号时立即消去,级联删除在一次遍历中完成。
我们用一个栈来维护当前字符串的状态。栈中的每个元素是一个二元组,表示某个字符及其连续出现的次数。
遍历字符串中的每个字符:
- 如果栈不为空且栈顶元素的字符与当前字符相同,则将栈顶元素的计数加一。
- 否则,将当前字符和计数 1 作为一个新元素压入栈中。
- 如果当前字符是
')',且栈中至少有两个元素,且栈顶元素的计数等于 ,且栈顶元素的前一个元素的计数大于等于 ,则弹出栈顶元素,并将前一个元素的计数减去 。如果前一个元素的计数变为 0,则也将其弹出。
遍历结束后,栈中剩下的元素即为最终字符串的状态。我们将这些元素按顺序连接起来,得到结果字符串。
时间复杂度 ,空间复杂度 ,其中 是字符串 的长度。
func removeSubstring(s string, k int) string {
type pair struct {
ch byte
count int
}
stk := make([]pair, 0)
for i := 0; i < len(s); i++ {
c := s[i]
if len(stk) > 0 && stk[len(stk)-1].ch == c {
stk[len(stk)-1].count++
} else {
stk = append(stk, pair{c, 1})
}
if c == ')' && len(stk) > 1 {
top := &stk[len(stk)-1]
prev := &stk[len(stk)-2]
if top.count == k && prev.count >= k {
stk = stk[:len(stk)-1]
prev.count -= k
if prev.count == 0 {
stk = stk[:len(stk)-1]
}
}
}
}
res := make([]byte, 0)
for _, p := range stk {
for i := 0; i < p.count; i++ {
res = append(res, p.ch)
}
}
return string(res)
}
3746. 等量移除后的字符串最小长度 ​
题目、示例与约束
给你一个仅由字符 'a' 和 'b' 组成的字符串 s。
你可以反复移除 任意子字符串 ,只要该子字符串中 'a' 和 'b' 的数量相等。每次移除后,剩余部分的字符串将无缝拼接在一起。
返回一个整数,表示经过任意次数的操作后,字符串可能的 最小长度 。
子字符串 是字符串中一个连续、非空的字符序列。
示例 1:
输入: s = "aabbab"
输出: 0
解释:
子字符串 "aabbab" 中有三个 'a' 和三个 'b'。由于它们的数量相等,可以直接移除整个字符串,最小长度为 0。
示例 2:
输入: s = "aaaa"
输出: 4
解释:
字符串 "aaaa" 中每个子字符串都仅包含 'a',无法移除任何部分,因此最小长度仍为 4。
示例 3:
输入: s = "aaabb"
输出: 1
解释:
首先移除子字符串 "ab",剩下 "aab"。然后再移除新的子字符串 "ab",剩下 "a"。无法再移除任何部分,因此最小长度为 1。
提示:
1 <= s.length <= 105s[i]是'a'或'b'。
方法一:计数 · Go 实现
思考
只要某段 个数相同即可整段删去,最终无法再删当且仅当剩下的全是同一种字符。因此答案是两种字符个数之差的绝对值,不必模拟删除过程。
根据题目描述,只要相邻字符不同,我们就可以移除它们。因此,最终剩下的字符串中只会包含相同字符,即全部是 'a' 或全部是 'b'。所以我们只需要计算字符串中 'a' 和 'b' 的数量,最终的最小长度就是两者数量的差的绝对值。
时间复杂度 ,其中 是字符串的长度。空间复杂度 。
func minLengthAfterRemovals(s string) int {
a := strings.Count(s, "a")
b := len(s) - a
return abs(a - b)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
3749. 计算有效表达式 🔒 ​
题目、示例与约束
给你一个字符串 expression 表示以简化形式嵌套的数学表达式。
有效 表达式要么是一个整数 字面量,要么符合 op(a,b) 格式,其中:
op是"add","sub","mul","div"中的一个。a和b都是有效表达式。
运算符 定义如下:
add(a,b) = a + bsub(a,b) = a - bmul(a,b) = a * bdiv(a,b) = a / b
返回一个整数,表示在完全计算表达式后的结果。
示例 1:
输入:expression = "add(2,3)"
输出:5
解释:
运算 add(2,3) 表示 2 + 3 = 5。
示例 2:
输入:expression = "-42"
输出:-42
解释:
这个表达式是单个整数字面量,所以结果是 -42。
示例 3:
输入:expression = "div(mul(4,sub(9,5)),add(1,1))"
输出:8
解释:
- 首先,计算内部表达式:
sub(9,5) = 9 - 5 = 4 - 接下来,将结果相乘:
mul(4,4) = 4 * 4 = 16 - 然后,计算右边的加法:
add(1,1) = 1 + 1 = 2 - 最后,将两个主要结果相除:
div(16,2) = 16 / 2 = 8
因此,整个表达式的结果是 8。
提示:
1 <= expression.length <= 105- 有效表达式
expression由数字、逗号、括号、减号'-'和小写字符串"add","sub","mul","div"组成。 - 所有中间结果都位于长整型的范围内。
- 所有除法运算结果都是整数值。
方法一:递归 · Go 实现
思考
表达式是嵌套的 ,结构由括号决定,适合递归下降。从当前位置若见到数字则解析字面量,否则读出操作符,再递归解析两个子表达式并按 计算。
我们定义一个递归函数 ,用于解析从索引 开始的子表达式并返回计算结果以及下一个未处理的索引位置。那么答案为 。
函数 的实现如下:
- 如果当前位置 处是一个数字或负号
-,则继续向后扫描直到遇到非数字字符,解析出一个整数并返回该整数以及下一个未处理的索引位置。 - 否则,当前位置 处是一个操作符
op的起始位置,我们继续向后扫描直到遇到左括号(,解析出操作符字符串op。然后跳过左括号,递归调用 解析第一个参数 ,再跳过逗号,递归调用 解析第二个参数 ,最后跳过右括号)。 - 根据操作符
op,计算 和 的结果,并返回该结果以及下一个未处理的索引位置。
时间复杂度 ,空间复杂度 。其中 是表达式字符串的长度。
func evaluateExpression(expression string) int64 {
var parse func(int) (int64, int)
parse = func(i int) (int64, int) {
if expression[i] >= '0' && expression[i] <= '9' || expression[i] == '-' {
j := i
if expression[j] == '-' {
j++
}
for j < len(expression) && expression[j] >= '0' && expression[j] <= '9' {
j++
}
num, _ := strconv.ParseInt(expression[i:j], 10, 64)
return num, j
}
j := i
for expression[j] != '(' {
j++
}
op := expression[i:j]
j++
val1, nextJ1 := parse(j)
j = nextJ1 + 1
val2, nextJ2 := parse(j)
j = nextJ2 + 1
var res int64
switch op {
case "add":
res = val1 + val2
case "sub":
res = val1 - val2
case "mul":
res = val1 * val2
case "div":
res = val1 / val2
}
return res, j
}
result, _ := parse(0)
return result
}
3834. 合并相邻且相等的元素 ​
题目、示例与约束
给你一个整数数组 nums。
你需要 重复 执行以下合并操作,直到无法再进行任何更改:
- 如果数组中存在 两个相邻且相等的元素,选择当前数组中 最左侧 的这对相邻元素,并用它们的 和 替换它们。
每次合并操作后,数组的大小 减少 1。对更新后的数组重复此过程,直到无法再进行任何操作。
返回完成所有可能的合并操作后的最终数组。
示例 1:
输入: nums = [3,1,1,2]
输出: [3,4]
解释:
- 中间的两个元素相等,将它们合并为
1 + 1 = 2,结果为[3, 2, 2]。 - 最后的两个元素相等,将它们合并为
2 + 2 = 4,结果为[3, 4]。 - 不再存在相邻且相等的元素。因此,答案为
[3, 4]。
示例 2:
输入: nums = [2,2,4]
输出: [8]
解释:
- 前两个元素相等,将它们合并为
2 + 2 = 4,结果为[4, 4]。 - 前两个元素相等,将它们合并为
4 + 4 = 8,结果为[8]。
示例 3:
输入: nums = [3,7,5]
输出: [3,7,5]
解释:
数组中没有相邻且相等的元素,因此不执行任何操作。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 105
方法一:栈 · Go 实现
思考
反复合并当前最左的相邻相等元为其和,直到不能再合。 ,每次从左扫描会平方。
每次合并只影响新和与其左侧是否再次相等,右侧尚未处理的元素不受影响。栈恰好保存「当前已稳定的左段」。
从左扫入栈,只要栈顶两元相等就弹出相加再压回,循环直到栈顶不再相等。
因题目总是合并最左一对,从左处理与反复取最左对等价,且每个元素进出栈常数次。
我们可以使用栈来模拟合并相邻且相等元素的过程。
定义一个栈 ,用于存储当前处理后的数组元素。遍历输入数组 的每个元素 ,将其压入栈中。然后检查栈顶的两个元素是否相等,如果相等,则将它们弹出并将它们的和重新压入栈中。重复此过程,直到栈顶的两个元素不再相等。最后,栈中的元素即为合并后的最终数组。
时间复杂度 ,其中 是数组 的长度。空间复杂度 ,用于存储栈中的元素。
func mergeAdjacent(nums []int) []int64 {
stk := []int64{}
for _, x := range nums {
stk = append(stk, int64(x))
for len(stk) > 1 && stk[len(stk)-1] == stk[len(stk)-2] {
a := stk[len(stk)-1]
stk = stk[:len(stk)-1]
b := stk[len(stk)-1]
stk = stk[:len(stk)-1]
stk = append(stk, a+b)
}
}
return stk
}
3835. 开销小于等于 K 的子数组数目 ​
题目、示例与约束
给你一个整数数组 nums,和一个整数 k。
对于任意子数组 nums[l..r],其 开销 定义为:
cost = (max(nums[l..r]) - min(nums[l..r])) * (r - l + 1)。
返回一个整数,表示 nums 中开销 小于或等于 k 的子数组数量。
子数组 是数组中连续的 非空 元素序列。
示例 1:
输入: nums = [1,3,2], k = 4
输出: 5
解释:
考虑 nums 的所有子数组:
nums[0..0]:cost = (1 - 1) * 1 = 0nums[0..1]:cost = (3 - 1) * 2 = 4nums[0..2]:cost = (3 - 1) * 3 = 6nums[1..1]:cost = (3 - 3) * 1 = 0nums[1..2]:cost = (3 - 2) * 2 = 2nums[2..2]:cost = (2 - 2) * 1 = 0
共有 5 个子数组的开销小于或等于 4。
示例 2:
输入: nums = [5,5,5,5], k = 0
输出: 10
解释:
对于 nums 的任何子数组,最大值和最小值都相同,因此开销始终为 0。
因此,nums 的每个子数组的开销都小于或等于 0。
对于长度为 4 的数组,子数组的总数为 (4 * 5) / 2 = 10。
示例 3:
输入: nums = [1,2,3], k = 0
输出: 3
解释:
nums 中开销为 0 的子数组仅包含单元素子数组,共有 3 个。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1090 <= k <= 1015
方法一:双端队列 + 枚举 + 双指针 · Go 实现
思考
子数组代价为 长度,计数代价 的个数。 ,枚举左右端点并维护最值过慢。
窗口变长时 与长度均不减,代价单调。固定右端后,合法左端是一段前缀。
用两个单调队列维护窗口最大、最小,左指针右移直到代价合法,则该右端贡献 个子数组。
每个下标进出队列一次,总线性。
我们注意到,如果一个子数组 的开销小于或等于 ,则对于任意 和 ,子数组 的开销也小于或等于 。因此,我们可以枚举右端点 ,使用双指针维护满足条件的最小左端点 ,那么以 结尾的满足条件的子数组数量为 ,我们将其累加到答案中即可。
我们可以使用两个双端队列分别维护当前窗口内的最大值和最小值。
时间复杂度 ,空间复杂度 。其中 是数组 的长度。
func countSubarrays(nums []int, k int64) int64 {
var ans int64 = 0
q1 := make([]int, 0)
q2 := make([]int, 0)
l := 0
for r, x := range nums {
for len(q1) > 0 && nums[q1[len(q1)-1]] <= x {
q1 = q1[:len(q1)-1]
}
for len(q2) > 0 && nums[q2[len(q2)-1]] >= x {
q2 = q2[:len(q2)-1]
}
q1 = append(q1, r)
q2 = append(q2, r)
for l < r &&
int64(nums[q1[0]]-nums[q2[0]])*int64(r-l+1) > k {
l++
if q1[0] < l {
q1 = q1[1:]
}
if q2[0] < l {
q2 = q2[1:]
}
}
ans += int64(r - l + 1)
}
return ans
}
3878. 统计好子数组 ​
题目、示例与约束
给你一个整数数组 nums。
如果一个 子数组 中所有元素的 按位或 等于该子数组中 至少出现一次 的元素,则称其为 好 子数组。
返回 nums 中好子数组的数量。
子数组 是数组中一段连续的 非空 元素序列。
这里,两个整数 a 和 b 的按位或表示为 a | b。
示例 1:
输入: nums = [4,2,3]
输出: 4
解释:
nums 的子数组有:
| 子数组 | 按位或 | 存在于子数组中 |
|---|---|---|
[4] |
4 = 4 |
是 |
[2] |
2 = 2 |
是 |
[3] |
3 = 3 |
是 |
[4, 2] |
4 | 2 = 6 |
否 |
[2, 3] |
2 | 3 = 3 |
是 |
[4, 2, 3] |
4 | 2 | 3 = 7 |
否 |
因此,nums 的好子数组是 [4]、[2]、[3] 和 [2, 3]。所以答案为 4。
示例 2:
输入: nums = [1,3,1]
输出: 6
解释:
nums 中任何包含 3 的子数组的按位或都等于 3,只包含 1 的子数组的按位或都等于 1。
在这两种情况下,结果都存在于子数组中,因此所有子数组都是好子数组,答案为 6。
提示:
1 <= nums.length <= 1050 <= nums[i] <= 109
方法一:单调栈 + 枚举贡献 · Go 实现
思考
好子数组的按位或等于其中某个元素。 ,不能枚举区间。
若或值等于 ,则区间内每个数都是 的位子集,且该区间须包含 。可用 作为「或值贡献者」。
单调栈找出 向左、向右能延伸到的最远边界,使中间元素均为 的子集。贡献为 。
每个元素作为或值被恰好统计在以其为「栈意义下控制者」的区间内。
我们可以枚举每个元素 作为子数组的按位或结果,统计有多少子数组的按位或恰好等于 。
若一个子数组的按位或为 ,则该子数组中的所有元素都必须满足:
即子数组中的所有元素必须是 的子集。我们可以使用单调栈来找到每个元素 的左边界 和右边界 ,使得在区间 内的元素都满足上述条件,且 和 不满足上述条件。则以 作为按位或结果的子数组数量为 。
时间复杂度 ,空间复杂度 ,其中 是数组的长度。
func countGoodSubarrays(nums []int) int64 {
n := len(nums)
l := make([]int, n)
for i := range l {
l[i] = -1
}
stk := []int{}
for i, x := range nums {
for len(stk) > 0 && nums[stk[len(stk)-1]] < x &&
(nums[stk[len(stk)-1]]|x) == x {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
l[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
r := make([]int, n)
for i := range r {
r[i] = n
}
stk = stk[:0]
for i := n - 1; i >= 0; i-- {
for len(stk) > 0 &&
(nums[stk[len(stk)-1]]|nums[i]) == nums[i] {
stk = stk[:len(stk)-1]
}
if len(stk) > 0 {
r[i] = stk[len(stk)-1]
}
stk = append(stk, i)
}
var ans int64
for i := 0; i < n; i++ {
ans += int64(i-l[i]) * int64(r[i]-i)
}
return ans
}
4045. 统计机器人组数 ​
题目、示例与约束
给你一个 严格递增 的整数数组 position,其中 position[i] 是第 i 个机器人(下标从 0 开始)在时间 t = 0 时的初始位置。
另给你一个整数数组 speed,其中 speed[i] 是第 i 个机器人的恒定速度(单位:单位/秒),以及一个整数 distance。
时间是连续的,以秒为单位。速度为 v 的机器人或机器人组在任意 t 秒的时间间隔内向右移动 v * t 个单位。
每当两个机器人或组之间的距离至多为 distance 时,它们就会合并成一个机器人组。
如果多个机器人或机器人组在同一时间满足合并条件,则所有合并 同时 发生。具体而言,任何相邻位置相差至多为 distance 的相连机器人或组都会合并为一个机器人组。
合并后,生成的机器人组将继承该组中 最右侧机器人 的当前位置和速度。一旦合并,机器人将永不分离。
返回在所有可能的合并发生后剩余的组数。
如果数组中的每个元素都严格大于其前一个元素(如果存在),则该数组是 严格递增 的。
示例 1:
输入: position = [1,5,6,20], speed = [4,3,2,3], distance = 1
输出: 2
解释:

- 最初,组为 {R1}、{R2}、{R3} 和 {R4}。
- 在
t = 0时,分别位于位置 5 和 6 的机器人 R2 和 R3 合并,因为它们相距 1 个单位。生成的组以最右侧机器人 R3 的位置和速度移动。现在的组为 {R1}、{R2, R3} 和 {R4}。 - 随后在
t = 2时,机器人 R1 追上组 {R2, R3} 并与其合并。现在的组为 {R1, R2, R3} 和 {R4}。
因此,答案是 2。
示例 2:
输入: position = [1,5,9], speed = [3,2,2], distance = 2
输出: 2
解释:

- 最初,组为 {R1}、{R2} 和 {R3}。
- 在
t = 2时,机器人 R1 追上机器人 R2 并与其合并。生成的组以最右侧机器人 R2 的位置和速度移动。现在的组为 {R1, R2} 和 {R3}。
因此,答案是 2。
示例 3:
输入: position = [9], speed = [8], distance = 5
输出: 1
解释:
最初只有一个组。因此,答案是 1。
提示:
1 <= position.length == speed.length <= 1051 <= position[i], speed[i], distance <= 109position严格递增。
方法一:Go 实现(灵茶山艾府) · Go 实现
灵茶山艾府 · 源码 · MIT 许可 · 原题解
方法一:正序遍历 + 栈
首先,找出在 时刻就合并的机器人:如果机器人 满足 或者 ,那么机器人 是「最右侧机器人」。
对于这些「最右侧机器人」,速度慢的机器人会在一定时间后与其左侧更快的机器人合并。我们可以把遍历过的机器人的速度保存在一个栈中,如果当前机器人的速度比栈顶小,那么栈顶机器人就会在一定时间后与当前机器人合并,于是弹出栈顶。反复直到栈为空,或者栈顶机器人的速度 当前机器人的速度。
最后,从栈底到栈顶,机器人的速度是递增的(允许相邻相等),每个机器人都追不上其右边的机器人,所以相邻机器人的距离始终大于 ,无法合并。最终答案为栈的大小。
本题视频讲解,欢迎点赞关注~
复杂度分析
- 时间复杂度:,其中 是 的长度。虽然我们写了个二重循环,但每个元素至多入栈出栈各一次,所以二重循环的总循环次数是 的,所以时间复杂度是 。
- 空间复杂度: 或 。如果把输入的数组当作栈用,可以做到 空间。
方法二:倒序遍历
速度小的机器人会把左边速度更大的机器人删除(合并)。
于是倒序遍历,维护遍历过的「最右侧机器人」速度的最小值 。
- 如果当前机器人的速度比 大,那它可以追上 ,不计入答案。
- 否则答案加一,更新 为当前机器人的速度。
复杂度分析
- 时间复杂度:,其中 是 的长度。
- 空间复杂度:。
相似题目
专题训练
- 双指针题单的「六、分组循环」。
- 数据结构题单的「§3.3 邻项消除」。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
// https://space.bilibili.com/206214
func countGroups1(position, speed []int, distance int) int {
st := speed[:0] // 原地栈
for i, p := range position {
// 找到这一组的最右侧机器人
if i == len(position)-1 || position[i+1]-p > distance {
v := speed[i]
for len(st) > 0 && st[len(st)-1] > v {
st = st[:len(st)-1] // 栈顶机器人速度更快,一段时间后与机器人 i 合并
}
st = append(st, v)
}
}
return len(st)
}
func countGroups(position, speed []int, distance int) int {
n := len(speed)
mn := speed[n-1]
ans := 1
for i := n - 2; i >= 0; i-- {
if speed[i] <= mn && position[i+1]-position[i] > distance {
mn = speed[i]
ans++
}
}
return ans
}
继续阅读
- 实践案例
- 实践案例
- 实践案例