01 / 本课学习路线
本课学习路线
阅读与推演约 116 分钟,练习约 47 分钟,进阶练习另需约 45 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
本课只有一个核心问题:哪端进、哪端出。括号检查、单调栈、解压缩都是「后进先出」的不同用法;P2490 则是判断一个队列是否还有序。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 说出括号题栈里存什么、哪三种情况判无效,通过(AC)P2600 | 第 04、08 节 | 自查第 2 条、必做任务 1 |
| 说出单调栈自底向上保持的不变量、弹栈时结算什么,通过 P2650 | 第 05、08 节 | 自查第 3 条、必做任务 2 |
| 解释单调栈整体 O(n):每个位置最多进出各一次 | 第 05 节补充 | 自查第 4 条 |
按「一端进出 / 两端进出」给栈、队列、deque 选型,并说出 list.pop(0) 为什么慢 | 第 03、06 节 | 自查第 5 条 |
| 用栈处理任意嵌套的解压缩(P2602)、用布尔状态代替队列模拟(P2490) | 第 06、07 节 | 进阶练习 1、2 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成模块 1 的「哈希计数」与「字符串规则模拟」。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | a = [1, 2, 3]; a.append(4); a.pop(); a.pop(0),三步之后 a 是什么?哪一步是 O(n)? | 列表常用操作 |
| 自测 2 | m = {")": "("},m.get("]") 返回什么?为什么不用 m["]"]? | 字典 |
| 自测 3 | for i, ch in enumerate("ab") 每轮的 i 和 ch 各是什么? | for 循环与字符串 |
| 自测 4 | from collections import deque; q = deque([1, 2]); q.appendleft(0); q.popleft() 之后 q 是什么? | 模块与 import |
| 自测 5 | 第一行是整数 n,接下来 2n 行每行一条文字指令,怎样读入并逐行拆开? | 标准输入输出与首次独立提交 |
展开先修自测答案
自测 1:依次 [1,2,3,4] → [1,2,3] → [2,3]。pop(0) 要把后面所有元素向前搬一格,是 O(n);append 与 pop() 都在尾部,O(1)。栈只用尾部;队列的出队要用 deque.popleft()。
自测 2:None。m["]"] 会抛 KeyError;get 让「栈顶与右括号不配对」的比较可以写成一句 stack[-1] != m.get(ch)。
自测 3:(0, 'a')、(1, 'b')。单调栈里存的就是这个 i;P2650 的输出正是这些下标。
自测 4:deque([1, 2]):左端加 0 再从左端弹出 0。两端操作都是 O(1)。
自测 5:lines = sys.stdin.read().split("\n"),第 0 行 int(lines[0]),之后每行 line.split() 得到 ["head", "add", "3"] 或 ["remove"]。P2490 的完整程序就是这样读的。
03 / 概念与术语
栈、队列、双端队列、单调栈、结算
四种容器只差在「哪端进、哪端出」。选型看题目对进出端的要求,不看名字。
| 术语 | 含义 | Python 写法 |
|---|---|---|
| 栈 | 后进先出:只在一端进出 | stack = [];append 入栈、pop() 出栈、stack[-1] 看栈顶 |
| 队列 | 先进先出:一端进、另一端出 | deque;append 入队、popleft() 出队 |
| 双端队列 | 两端都能进出 | deque;appendleft / popleft / append / pop |
| 单调栈 | 栈里存下标,自底向上对应的值保持单调(本课:不升) | while stack and v > values[stack[-1]]: 弹出并结算 |
| 结算 | 某个位置的答案在它被弹出的那一刻确定 | ans[j] = i |
| 不变量 | 每轮循环结束时都成立的性质:栈内值自底向上不升;栈里的位置都还没等到答案 | 写进注释 |
| 嵌套深度 | 当前有多少个左括号还没配对 = 栈的大小 | len(stack) |
| 题目对进出的要求 | 选什么 | 本课的题 |
|---|---|---|
| 只在一端进出(最近的先处理) | 栈(list) | P2600、P2602、P2650 的单调栈 |
| 一端进、另一端出 | 队列(deque) | 下一课的广度优先搜索 |
| 两端都要加、只从一端删 | 双端队列(deque),或像 P2490 那样根本不模拟 | P2490 |
| 按下标随机访问 | 普通列表(list) | 读入的身高数组 |
list.pop(0) 每次把后面全部元素向前搬一格:4 万个元素依次出队约 8×10⁸ 次搬移,Python 会超时。需要频繁从队头删除就用 deque;deque 按下标随机访问是 O(n),数组操作留给 list。
补充学习(选学)deque 与 list.pop(0) 的复杂度对比约 4 分钟隐藏的 O(n) 出队,和 deque 的适用边界
list.pop(0) 每次把后面所有元素向前搬一格,是 O(n);n 个元素依次全部出队,搬移总量约 n(n−1)/2。P2650 的上限 4 万人就是约 8×10⁸ 次——仅这一处就足够超时。
deque 两端进出都是 O(1),但按下标随机访问是 O(n)——队列的操作交给 deque,数组的操作留给普通列表(list),不要用 deque 做按下标的扫描。
04 / 括号检查:P2600
右括号找最近一个还没配对的左括号,栈的大小就是深度
P2600「括号检查」:一行只含六种括号的字符串(长度 0~10⁵);任一类型左右数量不等、或闭合顺序不对即无效,输出 0;有效则输出最大嵌套深度。题面示例:[] → 1;([]{()}) → 3;(]、([)]、)( → 0。
| 字符 | 动作 | 栈(底 → 顶) | 栈大小 | 最大深度 |
|---|---|---|---|---|
| ( | 入栈 | ( | 1 | 1 |
| [ | 入栈 | ( [ | 2 | 2 |
| ] | 栈顶 [ 配对 → 弹出 | ( | 1 | 2 |
| { | 入栈 | ( { | 2 | 2 |
| ( | 入栈 | ( { ( | 3 | 3 |
| ) | 栈顶 ( 配对 → 弹出 | ( { | 2 | 3 |
| } | 栈顶 { 配对 → 弹出 | ( | 1 | 3 |
| ) | 栈顶 ( 配对 → 弹出 | (空) | 0 | 3 |
扫完栈空 → 有效,输出 3。深度在入栈之后取:入栈前取会把 [] 算成 0(第 09 节错误表)。
| 输入 | 发现无效的时刻 | 原因 |
|---|---|---|
| )( | 第 1 个字符 ) | 栈空却来了右括号 |
| ([)] | 第 3 个字符 ) | 栈顶是 [,与 ) 类型不配对 |
| (() | 扫完 | 栈里还剩一个 ( 没配对 |
「每种括号左右数量相等」不等于有效:([)] 三种数量都相等,顺序却错了。所以不能只数个数,要用栈逐个配对。空串没有任何括号,栈始终为空,输出 0。
再算一组:(()(()))
字符: ( ( ) ( ( ) ) ) 栈大小: 1 2 1 2 3 2 1 0 最大深度 = 3;收尾栈空 → 有效,输出 3
括号深度练习模板
Pythonimport sys
def main() -> None:
s = sys.stdin.readline().strip()
stack = []
best = 0
for ch in s:
if ch in "([{":
stack.append(ch)
best = max(best, len(stack))
else:
# 待完成 1:空栈、或栈顶类型不配对——两种无效情形如何处理
...
# 待完成 2:收尾时还要检查什么,才能输出 best
...
main()栈自底向上、每个字符至多进出各一次:时间 O(n)、空间最坏 O(n)(全是左括号)。单调栈同理——while 套在 for 里看着像 O(n²),但每个下标至多进栈一次、被弹一次,合计不超过 2n 次。补看:有效括号动画。
05 / 单调栈:P2650
栈里存的是「还没等到答案的位置」,更高的人出现时连续弹出结算
P2650「找朋友」:第一行 N(0~40000),第二行 N 个身高;每个人向右找第一个比自己高的人,输出那个人的位置;找不到输出 0。题面示例:8 人 123 124 125 121 119 122 126 123 → 1 2 6 5 5 6 0 0;2 人 100 95 → 0 0。
输出的是下标(从 0 起),由题面示例确定
示例里第 0 个人(123)的朋友是第 1 个人(124),输出 1;第 2 个人(125)的朋友是第 6 个人(126),输出 6。所以输出的是 0 起的下标,「0」同时表示没有朋友——因为朋友一定在右边,下标 0 永远不会是答案,不会混淆。
| i | 身高 | 弹出并结算 | 处理后栈(下标·身高) |
|---|---|---|---|
| 0 | 123 | — | 0·123 |
| 1 | 124 | 弹 0:ans[0] = 1 | 1·124 |
| 2 | 125 | 弹 1:ans[1] = 2 | 2·125 |
| 3 | 121 | —(比栈顶矮,入栈等待) | 2·125, 3·121 |
| 4 | 119 | — | 2·125, 3·121, 4·119 |
| 5 | 122 | 弹 4:ans[4] = 5;弹 3:ans[3] = 5;122 < 125 停 | 2·125, 5·122 |
| 6 | 126 | 弹 5:ans[5] = 6;弹 2:ans[2] = 6 | 6·126 |
| 7 | 123 | — | 6·126, 7·123 |
扫完栈里剩 6、7,它们没等到更高的人,答案保持 0。输出 1 2 6 5 5 6 0 0,与题面一致。每个下标至多入栈一次、被弹一次——这就是 O(n) 的理由。
| i | 身高 | 弹出并结算 | 处理后栈 |
|---|---|---|---|
| 0 | 170 | — | 0·170 |
| 1 | 165 | — | 0·170, 1·165 |
| 2 | 180 | 弹 1:ans[1] = 2;弹 0:ans[0] = 2 | 2·180 |
| 3 | 175 | — | 2·180, 3·175 |
| 4 | 178 | 弹 3:ans[3] = 4;178 < 180 停 | 2·180, 4·178 |
输出 2 2 0 4 0。判定是严格大于:三个 170 的输出是 0 0 0,写成 ≥ 会得到 1 2 0。补看:每日温度动画(那题输出的是「还要等几步」,本题输出下标)。
补充学习(选学)单调栈为什么整体只有 O(n)约 5 分钟按每个下标的入栈和出栈次数分析复杂度,以及严格大于和 ≥ 的分界
复杂度要按元素统计:每个位置恰好进栈一次,之后最多被弹出一次,所以整段代码进栈加弹栈合计不超过 2n 次。题面示例 8 个人进栈 8 次、弹栈 6 次,剩下 2 个留在栈里到最后。
弹栈发生在「答案确定的那一刻」,这也是单调栈的读法:栈里留着的,全是还没等到答案的位置。判定用严格大于——等高不算「更高」,身高 170 170 170 的输出是 0 0 0;把判定写成 ≥ 是这类题不易察觉的答案错误(WA)。
补充学习(选学)参考实现:单调栈(做完推演再看)约 5 分钟下一个更大值下标函数(next_greater_index)+ 等待步数函数(daily_temperatures)完整实现,七组断言含等高与空输入
上面的推演表把 P2650 的进出栈过程逐行走了一遍,下面是把它写成函数的样子:返回每个位置右边第一个更大值的下标,等不到记 0——与 P2650 的输出完全一致。
单调栈的完整参考实现
Python# 单调栈的完整参考实现:每个位置右边第一个更大值的下标;等不到记 0
def next_greater_index(values):
n = len(values)
ans = [0] * n
stack = [] # 存下标;自底向上对应的值不升
for i, v in enumerate(values):
while stack and v > values[stack[-1]]: # 严格更大才结算
j = stack.pop() # 第 j 个位置等到了更大的值
ans[j] = i # 弹出的一刻结算:答案就是当前下标
stack.append(i)
return ans
assert next_greater_index([123, 124, 125, 121, 119, 122, 126, 123]) == [1, 2, 6, 5, 5, 6, 0, 0] # P2650 题面示例
assert next_greater_index([100, 95]) == [0, 0] # P2650 题面示例 2
assert next_greater_index([170, 165, 180, 175, 178]) == [2, 2, 0, 4, 0] # 第 05 节第二组
assert next_greater_index([170, 170, 170]) == [0, 0, 0] # 判定是严格大于:等高不算更高
assert next_greater_index([]) == []
# 同一个栈换一种结算:「还要等几步」(每日温度题的问法),等不到记 0
def daily_temperatures(temps):
idx = next_greater_index(temps)
return [j - i if j > i else 0 for i, j in enumerate(idx)]
assert daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73]) == [1, 1, 4, 2, 1, 1, 0, 0]
assert daily_temperatures([170, 165, 180, 175, 178]) == [2, 1, 0, 1, 0]七组断言分别覆盖:题面示例 1、题面示例 2、第 05 节第二组、全部等高(判定是严格大于,结果全 0)、空输入,以及同一个栈换成「还要等几步」结算的两组。每个下标至多进栈一次、被弹一次,整体是 O(n)。
06 / 双端队列:P2490
不必真的模拟:队列现在有没有序,一个布尔就够
P2490「特异性双端队列」:第一行 n(≤ 3×10⁵),接下来 2n 行指令——head add x / tail add x(依次加入 1 到 n)与 remove(只从头部取);要求取出顺序是 1 到 n,可以在任何时候把整个队列调整成有序,问最少调整几次。题面示例(n=5)→ 1。
关键观察:加入的数总是当前最大的。接在尾部永远不会打乱已有顺序;只有队列非空时从头部加入,才会把大数压到小数前面。于是只需维护「当前是否乱序」:非空时 head add → 乱序;tail add → 不变;remove 时若乱序就调整一次并置回有序。队列是否非空也不用真的存队列:已取走的个数记为 removed,队列里最小的数应是 removed + 1;head add x 时 x ≠ removed + 1 就说明里面还有比 x 小的数。
| 指令 | removed | 队列非空? | 乱序状态 | 调整次数 |
|---|---|---|---|---|
| head add 1 | 0 | 否(1 = 0 + 1) | 有序 | 0 |
| tail add 2 | 0 | — | 有序 | 0 |
| remove | 1 | — | 有序,直接取 1 | 0 |
| head add 3 | 1 | 是(3 ≠ 2) | 乱序 | 0 |
| tail add 4 | 1 | — | 乱序 | 0 |
| head add 5 | 1 | 是 | 乱序 | 0 |
| remove | 2 | — | 乱序 → 调整一次 → 有序,取 2 | 1 |
| remove ×3 | 5 | — | 有序,依次取 3、4、5 | 1 |
输出 1,与题面一致。想验证这个布尔写法,可以另写一个真实模拟(每次 remove 时队头不是期望值就整体排序并计数):两种写法在任何合法指令序列上结果相同,可用随机指令对拍。
第二组:什么时候需要两次调整
n=3: head add 1 → 有序 head add 2 → 非空、头部加入 → 乱序 remove → 调整 1 次,取 1 head add 3 → 队列里还有 2 → 乱序 remove → 调整 2 次,取 2 remove → 取 3;输出 2 对照 n=2: tail add 1, tail add 2, remove, remove → 尾部加入不打乱顺序,输出 0
07 / 嵌套解压缩:P2602
每层花括号一张草稿:遇到 { 开新层,遇到 }N 展开后拼回上一层
P2602「解压缩算法」:一行压缩串;字符后跟数字 N 表示重复 N 次,{…}N 表示花括号内容整体重复 N 次,可任意嵌套。题面示例:{A3B1{C}3}3 → AAABCCCAAABCCCAAABCCC;A3 → AAA。
| 读到 | 动作 | 栈(底 → 顶) |
|---|---|---|
| { | 开新层 | "" | "" |
| A3 | 当前层拼 AAA | "" | AAA |
| B1 | 当前层拼 B | "" | AAAB |
| { | 开新层 | "" | AAAB | "" |
| C | 没有数字按 1 次 | "" | AAAB | C |
| }3 | 弹出 C,重复 3 次拼回上一层 | "" | AAABCCC |
| }3 | 弹出 AAABCCC,重复 3 次拼回最外层 | AAABCCCAAABCCCAAABCCC |
栈底那张草稿就是答案。重复次数可能多位({AB}12),要连续读完整段数字;} 后面的数字属于整个花括号,不是括号内最后一个字符的。
复杂度:压缩串长 L、结果长 M、嵌套深度 D,时间 O(L + M·D)(每层闭合时把这层内容复制展开一次),空间 O(M)。题面没写输入长度上限,也没规定非法输入的输出——参考程序按「没有数字按 1 次」处理,题目页判题以真实用例为准。
08 / 从步骤到程序
四道题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。
| 题 | 步骤 | 代码 | 说明 |
|---|---|---|---|
| P2600 | 左括号入栈并取深度;右括号判空与配对;收尾判空 | stack.append(ch); best = max(best, len(stack));if not stack or stack[-1] != match.get(ch);if stack: valid = False | 三种无效各一行 |
| P2650 | 下标栈;严格更高就弹出结算 | while stack and h > heights[stack[-1]]: j = stack.pop(); ans[j] = i | 答案是下标 i,不加 1 |
| P2490 | 布尔乱序 + 已取走个数 | if x != removed + 1: disordered = True;remove 时 if disordered: adjust += 1 | 不建真实队列 |
| P2602 | { 开层、}N 展开拼回、字符 N 直接展开 | stack.append("");seg = stack.pop(); stack[-1] += seg * num;read_num 读整段数字 | 栈底是答案 |
展开完整参考程序 1:P2600 括号检查(先自己写完并提交一次,再展开对照)
完整程序:P2600(标准输入 → 标准输出)
Pythonimport sys
s = sys.stdin.readline().strip()
match = {")": "(", "]": "[", "}": "{"} # 右括号 → 应配对的左括号
stack = []
best = 0
valid = True
for ch in s:
if ch in "([{":
stack.append(ch)
best = max(best, len(stack)) # 入栈之后栈的大小就是当前嵌套深度
else:
if not stack or stack[-1] != match.get(ch): # 栈空、或栈顶类型不配对
valid = False
break
stack.pop()
if stack: # 扫完还有没配对的左括号
valid = False
print(best if valid else 0)自测建议:题面五组示例,再加空串(输出 0)与 (()(输出 0)。
展开完整参考程序 2:P2650 找朋友
完整程序:P2650(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
n = int(data[0])
heights = [int(x) for x in data[1:1 + n]]
ans = [0] * n # 找不到更高的人 → 0
stack = [] # 存下标;自底向上身高不升
for i, h in enumerate(heights):
while stack and h > heights[stack[-1]]: # 严格更高才结算
j = stack.pop()
ans[j] = i # 题面示例证明输出的是下标(从 0 起)
stack.append(i)
print(" ".join(str(x) for x in ans))自测建议:题面两组示例,以及 n=0(输出空行);想进一步验证,可写一个 O(n²) 向右扫描的暴力程序,用随机输入对拍。
展开完整参考程序 3:P2602 解压缩算法(进阶)
完整程序:P2602(标准输入 → 标准输出)
Pythonimport sys
s = sys.stdin.readline().strip()
def read_num(i): # 从位置 i 读一段连续数字;没有数字按 1
if i >= len(s) or not s[i].isdigit():
return 1, i
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
return num, i
stack = [""] # 每一层花括号一张「草稿」;栈底是最外层
i = 0
while i < len(s):
ch = s[i]
if ch == "{":
stack.append("") # 进入新的一层
i += 1
elif ch == "}":
num, i = read_num(i + 1) # } 后面的数字属于整个花括号
seg = stack.pop()
stack[-1] += seg * num # 展开后拼回上一层
else:
num, i = read_num(i + 1) # 普通字符后面的数字
stack[-1] += ch * num
print(stack[0])自测建议:题面两组示例、两位数重复次数 {AB}12,以及第 10 节练习 5 的三层嵌套。
展开完整参考程序 4:P2490 特异性双端队列(进阶)
完整程序:P2490(标准输入 → 标准输出)
Pythonimport sys
lines = sys.stdin.read().split("\n")
n = int(lines[0])
adjust = 0
disordered = False # 当前队列是否已经乱序(需要在下次取数前调整)
removed = 0 # 已经取走了几个数 → 队列里最小的数应是 removed + 1
for line in lines[1:1 + 2 * n]:
parts = line.split()
if parts[0] == "remove":
if disordered:
adjust += 1 # 取数前调整一次,整队变有序
disordered = False
removed += 1
elif parts[0] == "head":
x = int(parts[2])
if x != removed + 1: # 队列非空(里面还有比 x 小的数)时从头部加入 → 乱序
disordered = True
# tail add:加入的数总是最大的,接在尾部不打乱顺序
print(adjust)自测建议:题面示例(输出 1)、第 06 节第二组(输出 2);想进一步验证,可与「真实双端队列 + 队头不对就整体排序」的模拟对拍。
09 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| P2600 只数每种括号的个数是否相等 | ([)] | 2 | 0 | 答案错误(WA) |
| P2600 入栈之前取深度 | [] | 0 | 1 | 答案错误(WA) |
| P2600 漏掉收尾「栈非空即无效」 | (() | 2 | 0 | 答案错误(WA) |
| P2600 右括号来时不判空栈直接 pop | )( | 抛出 IndexError | 0 | 运行错误(RE) |
| P2650 判定写成 ≥ | 3 / 170 170 170 | 1 2 0 | 0 0 0 | 答案错误(WA) |
| P2650 输出下标加 1(以为位置从 1 起) | 题面示例 | 2 3 7 6 6 7 0 0 | 1 2 6 5 5 6 0 0 | 答案错误(WA) |
| P2650 双重循环向右找 | n = 4×10⁴ 且身高递减 | 结果正确但约 8×10⁸ 次比较 | 同左 | 超时(TLE) |
| P2602 重复次数只读一位 | {AB}12 | AB2 | ABABABABABABABABABABABAB | 答案错误(WA) |
| P2490 头部加入一律算乱序(不判队列是否为空) | 题面示例 | 2 | 1 | 答案错误(WA) |
| P2490 用 list.pop(0) 真实模拟并每次排序 | n = 3×10⁵ | 结果正确但每次出队 O(n) | 同左 | 超时(TLE) |
第一行的 2:([)] 三种括号数量都相等,只数个数会当成有效,深度按左括号累加得 2。第六行:题面示例已确定输出下标从 0 起。
| 做法 | 时间 | n = 4×10⁴ 时 |
|---|---|---|
| 向右逐个找更高的人 | O(n²) | ≈ 8×10⁸,超时 |
| 单调栈 | O(n) | 进栈 + 弹栈 ≤ 8×10⁴ 次 |
list.pop(0) 当队列 | 每次 O(n) | 全部出队 ≈ 8×10⁸ 次搬移 |
deque.popleft() | 每次 O(1) | ≈ 4×10⁴ |
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 04 节表的格式,对 {[()]}() 与 {[(])} 逐字符列出栈与深度,写出输出。
展开练习 1 答案
{[()]}():栈大小依次 1 2 3 2 1 0 1 0,全部配对成功,最大深度 3 → 输出 3。{[(])}:读到第 4 个字符 ] 时栈顶是 (,类型不配对 → 输出 0。
练习 2(改一个条件):P2650 改成「向右找第一个比自己矮的人」,单调栈的判定和不变量各怎么改?对 170 165 180 175 178 写出输出。
展开练习 2 答案
判定改成 h < heights[stack[-1]](严格更矮才弹出),栈内自底向上身高不降。逐位置:0·170 入;1·165 < 170 → 弹 0:ans[0] = 1;2·180 入;3·175 < 180 → 弹 2:ans[2] = 3;4·178 入。输出 1 0 3 0 0。
练习 3(改一个条件):改成「向左找第一个比自己高的人」。提示:从右往左扫描,或者从左往右扫描时答案在入栈前看栈顶。对同一组身高写出输出。
展开练习 3 答案
从左往右:先弹出所有不高于当前的下标(它们不可能是任何右侧元素的「左边第一个更高」),弹完后栈顶就是当前元素的答案(栈空则 0),再入栈。逐位置:0·170 → 栈空 0;1·165 → 栈顶 0 高于它 → 0?注意 0 是下标也是「没有」,本变式用下标从 0 起会混淆,改为输出下标 + 1 或 −1;按 −1 表示没有:170 → −1;165 → 0;180 → −1;175 → 2;178 → 2。输出 -1 0 -1 2 2。这正是 P2650 用 0 兼作「没有」的前提:答案一定在右边,下标 0 不会出现。
练习 4(独立实现):完成「代码自测」的 max_depth,再加两条断言:题面示例 ([]{()}) 应为 3,([)] 应为 0。
展开练习 4 答案
max_depth 的参考实现(自带断言)
Pythondef max_depth(s):
match = {")": "(", "]": "[", "}": "{"}
stack = []
best = 0
for ch in s:
if ch in "([{":
stack.append(ch)
best = max(best, len(stack)) # 入栈之后再取最大值
else:
if not stack or stack[-1] != match.get(ch):
return 0 # 栈空 / 类型不配对 → 无效
stack.pop()
return best if not stack else 0 # 收尾有剩 → 无效
assert max_depth("(()(()))") == 3
assert max_depth(")(") == 0 # 右括号先出现,无效
assert max_depth("(()") == 0 # 收尾栈非空,无效
assert max_depth("") == 0 # 空串按题目要求处理
assert max_depth("([]{()})") == 3 # 题面示例
assert max_depth("([)]") == 0 # 数量相等但顺序错六条断言覆盖:常规、右括号先出现、收尾有剩、空串、题面示例、数量相等顺序错。
练习 5(迁移):不运行程序,按第 07 节表的格式手算 {A2{B{C}2}2}2 的解压结果;再按第 06 节表的格式手算 P2490 指令 n=3:tail add 1 / head add 2 / remove / tail add 3 / remove / remove。
展开练习 5 答案
解压:最内层 {C}2 → CC;B 后接 CC 得 BCC,{BCC}2 → BCCBCC;A2 → AA,这层是 AABCCBCC;整体重复 2 次 → AABCCBCCAABCCBCC(16 个字符)。P2490:tail add 1 → 有序;head add 2(2 ≠ 0 + 1,非空)→ 乱序;remove → 调整 1 次,取 1;tail add 3 → 不变(有序);remove → 取 2;remove → 取 3。输出 1。
11 / 读题要求与复习自评
四道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | P2600 | P2650 | P2602 | P2490 |
|---|---|---|---|---|
| 输入 | 一行括号串(可为空) | N;N 个身高 | 一行压缩串 | n;2n 行指令 |
| 输出 | 最大深度;无效 0 | N 个下标(从 0 起)空格分隔;没有 0 | 解压后的串 | 最少调整次数 |
| 容器 | 栈 | 单调栈(存下标) | 栈(每层一张草稿) | 布尔状态(不建队列) |
| 数据范围 | 长度 ≤ 10⁵ | N ≤ 4×10⁴ | 题目页为准 | n ≤ 3×10⁵ |
| 样例 | ([]{()}) → 3;([)] → 0 | 8 / 123 124 125 121 119 122 126 123 → 1 2 6 5 5 6 0 0 | {A3B1{C}3}3 → AAABCCCAAABCCCAAABCCC | 题面 10 行指令 → 1 |
需要对照解法时,展开本课第 08 节的完整参考程序;题目页另有思路与参考代码,可在题目页查看。完成条件:本课算完成 = P2600、P2650 两道必做题都通过判题,并勾选全部六条「学习完成检查」;六条检查是自评,勾选不改变题目的通过(AC)状态;进阶练习与复习题单独统计,不影响完成状态。复习时用三个问题自测:① 不看正文,说出括号题的三种无效各在哪一步发现;② 不看表格,重推 170 165 180 175 178 的单调栈并写出输出;③ 说出 P2490 为什么头部加入才会乱序、尾部加入不会。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
基础加练(选做)
同一主题的 3 道基础题
这组题目采用标准输入输出(ACM 模式):程序从标准输入读取数据、把结果写到标准输出。它们难度低于必做题,适合在必做题之前热身,或在未通过时回来巩固;不计入本课完成标准。每道题给出练习重点、完成标准与提示;已在本站通过的题目会直接显示为已通过。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/2 道;进阶练习已通过 0/2 道
括号深度与无效判定(max_depth)
代码自测自主练习练习重点:入栈后统计深度;两处无效判定(配不上、收尾有剩);预计用时:12 分钟
完成标准:能说出三种判无效的情况各发生在代码哪一行
需要时查看提示
右括号来时栈空、或栈顶类型不配对,立刻无效;整串扫完栈没清空,也无效。深度在入栈之后取最大值,不在入栈之前取。参考实现在第 10 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
def max_depth(s):
# 你来写:返回括号最大嵌套深度;无效串返回 0
...
assert max_depth("(()(()))") == 3
assert max_depth(")(") == 0 # 右括号先出现,无效
assert max_depth("(()") == 0 # 收尾栈非空,无效
assert max_depth("") == 0 # 空串按题目要求处理P2600 · 括号检查
必做任务 1练习重点:三种括号混合配对 + 最大嵌套深度,无效输出 0;预计用时:15 分钟
完成标准:能解释为什么「数量相等」不等于「顺序正确」
需要时查看提示
数量相等但顺序错的例子:([)]。所以不能只数括号个数,要用栈逐个配对。六种括号的类型配对用字典存:右括号 → 对应左括号。第 04 节把题面示例逐字符列出。
P2650 · 找朋友
必做任务 2练习重点:栈里存下标;更高的人出现时连续弹出结算;预计用时:20 分钟
完成标准:能解释每个位置最多进栈出栈各一次,整体 O(n)
需要时查看提示
输出的是朋友的下标(从 0 起),没有朋友输出 0——以题面示例 1 2 6 5 5 6 0 0 为准,不要加 1。判定是严格大于:等高不弹出。n 到 4 万,双重循环最坏 8×10⁸ 次比较,要用单调栈。第 05 节有逐位置推演表。
P2602 · 解压缩算法
进阶练习 1进阶练习练习重点:花括号任意嵌套:遇到 { 开新层,遇到 }N 展开后拼回上一层;预计用时:25 分钟
完成标准:能说出栈里每层保存的是什么(这层已经还原的串)
需要时查看提示
遇到 { 压入一张空草稿;遇到 }N 弹出栈顶这层,重复 N 次拼到上一层末尾;字符后跟数字在当前层直接展开。数字要连续读完整段({AB}12 是 12 次)。第 07 节用题面示例逐步列出栈的内容。
P2490 · 特异性双端队列
进阶练习 2进阶练习练习重点:把「两头可加、只从头删」翻译成一个布尔状态:队列现在有没有序;预计用时:20 分钟
完成标准:能说出头部加入和尾部加入对顺序的影响为什么不一样
需要时查看提示
加入的数递增,所以从尾部加入永远不会打乱已有顺序;只有队列非空时从头部加入才会乱序。队列是否非空用「已取走个数 + 1 是否等于加入的数」判断,不必真的存队列。第 06 节把题面示例逐指令列出,答案 1。
提交结果
提交结果说明与处理方法
- WA
答案错误
优先检查三处:P2600 无效串没输出 0、P2650 输出的下标加了 1 或没输出 0、单调栈判定写成了 ≥——第 09 节的表给出了每种错误的具体输出
- PE
格式错误
P2650 是一行空格分隔的 n 个数——行尾多一个空格逐字节比对就不一致;n=0 输出空行
- RE
运行错误
右括号到来时不判空栈直接 pop;deque 为空时 popleft
- TLE
超时
list.pop(0) 当队列用、或「向右找更大」写了双重循环——4 万规模都撑不住
- AC
通过
再测空串、全左括号、全等身高三个边界;记下本课的首次错误类型
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。