1. 五级首题:为什么考了一道“数据结构热身题”
GESP2026年C++五级编程题第一题,在考后讨论里热度比想象中高。原以为是道送分题,结果不少考生反馈“样例过了,交上去却拿不到满分”,问题几乎都出在边界处理上。这其实非常符合五级首题的定位:它不是压轴难题,不需要动态规划,也不涉及图论,但恰好能把“读题是否仔细、基础数据结构是否熟练、边界意识是否到位”一次全部检验出来。
五级的难度介于四级和六级之间。四级还在培养基本的枚举、模拟、简单排序能力,六级开始真正接触树、图、动态规划的入门模型。而五级处在中间的衔接带,最重要的任务就是建立“用标准数据结构去解决规则问题”的思维。第一题承担的是稳定心态的职责,不可能一上来就是复杂算法,所以命题人最喜欢做的,就是从三类模板里选一种考:栈(括号匹配、表达式求值的简化版)、字典表(字符串词频统计、排名去重)、队列或双端队列(固定规则的模拟流程)。
2026年这道题走的就是“字符串 + 栈”的路线。放在2026年这个时间点看,这个选择一点都不意外:栈是五级大纲里的核心数据结构,括号匹配又是栈最经典、最没有争议的应用场景。代码量可以控制在几十行以内,又能自然延伸出嵌套深度、合法性判断、非法序列输出等多个考点,非常适合作为全卷第一题,给认真准备过的学生送分,给靠蒙或死记模板的学生一记响亮的提醒。
我在辅导中反复和学生说:这种题的训练价值不在“会不会写”,而在“能不能一次写对”。很多同学看到括号匹配就觉得简单,结果不是忘了判空栈,就是没仔细处理右括号与左括号的类型对应关系,又或者在算最大深度时更新时机错了。下面我以这道题的核心考法——混合括号序列的合法性与最大嵌套深度——作为原型,完整还原从审题到AC代码再到自测的全过程。这套思路可以直接套用到绝大多数基于栈的字符串处理题上。
2. 题目原型:括号序列到底要输出什么
2.1 题目描述的还原版本
根据考生回忆和命题风格整理,这道题的大意如下:
输入一行只包含六种字符的字符串 s,字符分别是
'(' ')' '[' ']' '{' '}',长度满足 1 ≤ |s| ≤ 10^5。
如果字符串是一个合法的括号序列,输出它的最大嵌套深度;如果不是合法的括号序列,输出 -1。
这里需要解释两个关键概念,因为它们直接决定算法怎么写。
合法括号序列:每个右括号都要和它“左侧最近、尚未闭合、且类型相同”的左括号配对。左括号必须先出现的后闭合,也就是遵循栈的先进后出规则。像([{}])这种层层嵌套的序列是合法的;而([)]虽然左右括号数量完全相等,却因为类型交叉配对不合法。
最大嵌套深度:在扫描过程中,同一时刻连续嵌套且尚未闭合的左括号数量就是当前深度,整个过程中出现的最大深度就是答案。([{}])的深度会一路从1加到3,所以输出3。
2.2 这道题真正在考察什么
表面上看考的是栈,实际上考的是两个子问题的组合:
- 合法性判定:类型匹配加顺序匹配,二者缺一不可。
- 深度计算:在合法性成立的前提下,深度就等价于栈能到达的最大高度。
这两个子问题被巧妙地糅在了一道题里:如果你只写一个计数器去统计括号数量,那你能拿到“判断左右数量相等”这部分思路分,但会挂在([)]这种样例上;如果你只知道用栈判断合法性,却不知道在压栈过程中顺手记录最大深度,又会漏掉第二个子问题的分数。
所以正确的思考顺序是:先把“合法性”和“深度”当成两个独立的输出信号,再去想怎么用一个栈同时完成。千万不要一上来就想“我要输出最大深度”,于是只维护一个整数计数器,那样会把类型匹配信息丢掉。栈在这里不是可选项,而是必须项,因为它保存的是完整的、按顺序排列的未闭合左括号序列。
2.3 一个手把手的推演
拿样例([{}])来说,扫描过程是这样的:
- 遇到
(:入栈,当前深度1,最大深度更新为1。 - 遇到
[:入栈,当前深度2,最大深度更新为2。 - 遇到
{:入栈,当前深度3,最大深度更新为3。 - 遇到
}:栈顶是{,类型匹配,弹出,当前深度回到2。 - 遇到
]:栈顶是[,类型匹配,弹出,当前深度回到1。 - 遇到
):栈顶是(,类型匹配,弹出,当前深度回到0。 - 扫描结束,栈为空,序列合法,输出最大深度3。
再看非法的([)]:
- 遇到
(:入栈,深度1。 - 遇到
[:入栈,深度2。 - 遇到
):此时栈顶是[,但右括号)期望匹配的是(,类型不匹配,直接输出-1。
这个推演过程看起来简单,但建议每个人都在草稿纸上亲手模拟一遍。我在实际教学里发现,能完整写出这个推演过程的学生,代码基本不会出原则性错误;反过来,跳过推演直接开写的人,往往会在某个角落漏掉判断。
3. 解题主线:为什么非用栈不可,以及三个终止条件
3.1 用计数器为什么不行
初学者最容易提出的方案是:遇到左括号计数加一,遇到右括号计数减一,如果最终计数为零就认为合法,过程中记录最大值就是答案。这个思路对同类括号((()))是有效的,但一旦引入多类型括号立刻失效。
问题出在“类型对应关系”上。计数器的状态只是一个数字,它不可能记住“目前有一个[还没闭合,它的上面还有一个(”。当遇到)时,计数器只知道当前未闭合左括号总数是2,却不知道栈顶到底是(还是[。所以([)]这种左右数量完全成对、但顺序错误的序列,计数器方案会错误地判断为合法。
栈的价值就在于:它保存的不只是数量,而是完整的未闭合左括号序列。栈顶就是“最近的、还没闭合的左括号”,右括号到达时只需要看栈顶一个元素,就能完成类型匹配判断。这个特性完美对应题目中“左侧最近且尚未闭合”的定义。
3.2 主算法流程
整个算法用一段伪代码就能说清楚:
dep = 0 maxDep = 0 stack st for ch in s: if ch 是左括号: st.push(ch) dep = dep + 1 maxDep = max(maxDep, dep) else: if st 为空: 输出 -1,结束 if st.top() 不是与 ch 匹配的左括号: 输出 -1,结束 st.pop() dep = dep - 1 if st 非空: 输出 -1 else: 输出 maxDep这段伪代码里有三个终止条件,任何一个触发都说明序列非法,直接输出-1:
- 遇到右括号时栈为空:说明当前右括号没有可匹配的左括号,典型的例子就是
))(。 - 遇到右括号时栈顶类型不匹配:说明交叉配对,典型的例子就是
([)]。 - 扫描结束后栈非空:说明存在没有被闭合的左括号,典型的例子就是
((()。
这三个条件顺序一个都不能调换。先判空再判类型,是因为空栈时根本不存在栈顶元素;先判类型再判空,则会在空栈时访问不存在的栈顶,造成运行时错误。
3.3 最大深度为什么要在入栈时更新
很多同学会疑惑:为什么maxDep的更新放在入栈分支,而不是出栈分支?道理很简单:当前深度是由“未闭合的左括号数量”决定的,而这个数量只会在左括号出现时增加。每压入一个左括号,就说明嵌套层数爬高了一层。出栈只是下降过程,不可能创造新的最大深度。
如果你只在出栈时更新maxDep,那么像([{}])这种序列,出栈时深度分别是从3降到2、从2降到1、从1降到0,取到的最大值是2,正确答案3就被漏掉了。这个细节属于“代码结构正确但业务逻辑错误”的典型问题,样例可能能过,但换一组数据就露馅。
4. 完整C++实现与逐段注解
4.1 考场可用的AC版本
下面这版代码是我在考场环境下会直接提交的版本,追求的是“短、清晰、不容易写错”:
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); string s; cin >> s; unordered_map<char, char> match = { {')', '('}, {']', '['}, {'}', '{'} }; stack<char> st; int dep = 0; int maxDep = 0; for (char ch : s) { if (ch == '(' || ch == '[' || ch == '{') { st.push(ch); dep++; if (dep > maxDep) { maxDep = dep; } } else { if (st.empty()) { cout << -1 << '\n'; return 0; } if (st.top() != match[ch]) { cout << -1 << '\n'; return 0; } st.pop(); dep--; } } if (!st.empty()) { cout << -1 << '\n'; return 0; } cout << maxDep << '\n'; return 0; }4.2 关键写法说明
头文件与万能头:#include <bits/stdc++.h>是竞赛编程里常用的万能头文件,官方编译环境通常支持。如果你们本地的练习环境不支持,可以拆开写成#include <iostream>、#include <stack>、#include <unordered_map>、#include <string>,效果完全一样。
类型匹配的映射表:我用unordered_map把右括号映射到对应的左括号,这样主循环里只需要一句st.top() != match[ch]就能完成类型判断。如果不喜欢这种写法,也可以写成函数:
char getMatch(char ch) { if (ch == ')') return '('; if (ch == ']') return '['; if (ch == '}') return '{'; return '\0'; }两种方式等价,但映射表在代码量更大的题目里更容易扩展。比如未来要加一种新的括号类型,映射表只需要加一行,而if-else链会越写越长。
输入输出同步关闭:ios::sync_with_stdio(false); cin.tie(nullptr);这两行在往年GESP里不一定非写不可,因为第一题的字符串长度只有10^5。但从养成习惯的角度,我建议每道题都写。它能让cin/cout的输入输出速度快出不少,避免在数据量稍大的测试点出现不必要的超时。
关于match[ch]的安全性:当ch是右括号时,match里一定有对应的键,不会插入新键。题目保证字符串只包含六种字符,所以不用担心中间混入空格或其它符号。
4.3 一个可维护性更好的函数化版本
如果你还在上机练习阶段,我更推荐把核心逻辑封装成函数,这样以后遇到类似题目可以直接复用:
bool check(const string& s, int& maxDep) { stack<char> st; unordered_map<char, char> match = { {')', '('}, {']', '['}, {'}', '{'} }; int dep = 0; maxDep = 0; for (char ch : s) { if (ch == '(' || ch == '[' || ch == '{') { st.push(ch); dep++; maxDep = max(maxDep, dep); } else { if (st.empty() || st.top() != match[ch]) { return false; } st.pop(); dep--; } } return st.empty(); }main里只需要调用函数根据布尔结果输出即可。这种结构的好处是职责单一:主程序只管输入输出,合法性判断与深度计算全部封装在内。考试时时间紧张可能没必要再封装一层,但平时训练建议这么做,因为五级之后的题目复杂度会明显上升,尽早培养“函数拆分逻辑”的意识,后面学递归、学分治都会更顺。
5. 测试用例设计:从样例到极端边界,一个都不能少
5.1 我自己一定会跑的一组用例
这道题样例通常只有两三组,覆盖面非常有限。我自己在上考场前会把下面这组用例背下来,每写一道匹配题都会拿它们快速自测:
| 输入 | 预期输出 | 说明 |
|---|---|---|
() | 1 | 最基础的一层嵌套 |
([{}]) | 3 | 不同类型逐层嵌套,最典型的合法样例 |
([)] | -1 | 左右数量相等但类型交叉,最容易漏判 |
(((()))) | 4 | 同一类型多层嵌套 |
((((((())))))) | 7 | 嵌套深度较大,验证最大深度统计(7个左括号+7个右括号) |
((() | -1 | 结束后栈不为空,左括号未闭合 |
))) | -1 | 一开始就遇到右括号,需要判空栈 |
(){[]} | 2 | 合法但深度先降后升,验证最大深度不会被低估 |
{}([]){} | 2 | 多段交替匹配,检查栈能否正确清空 |
这里特别提一下(){[]}这个用例:它的深度变化是1→2→1→2→1,真正的最大值出现在第二段[]处。如果代码只在入栈时更新maxDep,这个用例恰好能验证“最大深度是否会被中间出栈操作影响”。如果只在出栈时更新,它也会暴露错误。
5.2 边界用例为什么能救命
字符串长度上限是10^5,所以理论上最大深度不会超过10^5,用int存储完全够用,不需要long long。真正的风险不在于整数溢出,而在于逻辑边界:
- 空栈出栈:
)))这种输入会让没写判空代码的程序崩溃。在 C++ 里对空栈调用top()或pop()属于未定义行为,轻则输出错误,重则直接报运行时错误。 - 结束后栈有残留:
((()这种输入,如果不做最终判空,程序会输出一个正数,但实际上应该输出-1。 - 最大深度出现在最后:
((((((()))))))这种输入,只有入栈时更新maxDep才能正确得到7。
这些边界用例的价值在于:它们能把“代码能跑”和“代码正确”区分开。我在初学阶段也吃过亏,样例全过,交上去错了一大半,就是因为测试用例没覆盖到这些极端情况。
6. 考场经验:这道题真正在淘汰什么样的人
6.1 高频丢分点清单
考完复盘时,我听到最多的丢分原因集中在这几个地方:
- 没判空栈直接
pop。这是最严重的运行时错误,一出现整个测试点就是0分。 - 只统计括号数量不记录类型。样例可能是
(()())之类的纯圆括号样例,代码在样例上没问题,一到混合类型就挂。 - 最大深度只在出栈时更新。最终答案永远比正确答案小1到若干,输出结果完全错误。
- 扫描结束后忘记检查栈是否为空。遇到只有左括号的测试点,输出错误的正数。
- 使用字符数组时空间开小了。有的同学还在用
char s[100]接长度10^5的输入,直接越界。
很多人觉得括号匹配“太简单不用专门练”,但实际上它考察的就是基本功扎不扎实。一道如此简单的题,区分度却很高:基础扎实的同学五分钟AC,靠背模板的同学可能卡二十分钟还查不出错在哪。
6.2 考场上的提交顺序建议
我的建议是:拿到试卷先花两分钟通读全部题目,然后回到第一题,按下面的顺序推进:
- 读题两分钟:圈出输入规模和输出条件,特别是“非法时输出-1”这种说明。
- 手推一个例子:拿样例的合法序列和非合法序列各推一遍,把栈的变化过程写在草稿纸上。
- 写代码:按照“入栈分支-出栈分支-最终判空”的三段结构写,写完后对照伪代码检查一遍。
- 跑自测用例:至少跑三组——一组合法嵌套、一组类型交叉非法、一组单边括号非法。
这套流程加起来的耗时,熟练以后大约十到十五分钟。不要小看这十五分钟,它能决定你整个卷面的心态。第一题稳了,后面就算遇到难题,心里也不会慌。
6.3 给第一次参加五级的同学一句提醒
不要在考试时临时创造“更聪明的写法”。栈是这道题的标准答案,也是经过无数人验证的最可靠方案。考场上最怕的就是觉得“用栈太简单,我是不是应该写个递归”,然后在递归里System崩溃。第一题要的是正确的、可控的、不给自己找麻烦的解法,越朴素越好。至于优雅和高性能,那是保证正确之后才有资格追求的东西。
7. 进阶:如果第一题变成“多次区间查询”,思路怎么升级
最后留一个扩展思考,给那些觉得第一题太基础、想继续冲六级和七级的同学。假如题目加一个条件:给定 m 次询问,每次给一个区间 [l, r],问这个子串是否合法、最大深度是多少。直接对每个询问都扫描一遍子串,复杂度是 O(mn),在 n 和 m 都到10^5时会超时。
这类问题的经典切入思路有两个方向:
- 前缀和思想:把左括号视为+1,右括号视为-1,那么一个区间要合法,首先整体和为0;其次任意前缀和不能变成负数。配合预处理前缀和数组以及区间最小值查询,可以快速判断合法性。
- 线段树或树状数组:每个位置可以抽象成一个权重值,区间最大深度可以转化为维护最大前缀和与最小前缀和的组合运算。线段树节点存储区间前缀和的最小值最大值,就能在 O(log n) 时间内回答一次询问。
这个方向就不再展开了,因为它已经超出五级第一题的考纲范围。但我想强调一点:括号匹配并不是一道孤立的小题,它背后隐藏着“前缀和模型”“区间最值查询”“线段树信息合并”这一整条升级路线。五级把它作为第一题,是一个非常好的“地基题”。把这套栈思路彻底吃透,后面学习更复杂的区间数据结构时,你会发现很多概念都能连接到今天这道题上。
按我自己刷题的经验,把一道简单题做到“不丢分、完全掌握、还能勾连出扩展方向”,比囫囵吞枣刷十道难题更有价值。2026年这道五级首题就是这种值得多花一点时间的题。