ACM 一个月学习计划 · DAY 28

Day 28 · 栈与队列:STL 容器

栈是「一摞盘子」,队列是「排队买饭」—— 两种最简单的规则,却能解决括号匹配、表达式求值这类经典问题

今日目标:背熟 stack / queue / priority_queue 的接口,会写括号匹配和后缀表达式求值
预计用时:约 3 小时(含做题)  难度:普及-
题目来源:洛谷 B3614 / B3616 / P1449 / P1739
← Day 27 · 完全背包 + DP 综合

⏱今日时间安排

0 - 30 min理解 LIFO / FIFO 两种规则,背下接口(第 1、2 节)
30 - 45 min三个容器的接口对照表(第 3 节)—— 考试前扫一眼
45 - 75 min括号匹配 + 后缀表达式两个模板(第 4、5 节)
75 - 150 min做 4 道题,每题限时 20 分钟
150 - 180 min对照本文题解补题 + 做自测
💡 和前面内容的衔接

从 Day 1 到今天,你用的容器只有「数组」。数组的特点是:想访问哪里就访问哪里(a[7] 随便读), 但你也得自己管下标。

今天要学的两个容器刚好相反:你只能碰「一头」。听起来像是限制,但正是这种限制让某些问题变得特别简单 —— 比如「最近出现的那个还没配对的左括号是哪个」,答案显然就是「最后一个」,也就是栈顶。

1栈 stack:后进先出(LIFO)

先打个比方

食堂里有一摞干净盘子。阿姨洗完一个就往最上面放一个;你要拿盘子,也只能从最上面拿。 最后放上去的那个,最先被拿走。

这就是栈:Last In First Out(后进先出),简称 LIFO。 能操作的那一端叫栈顶(top),另一端叫栈底。

操作写法作用
入栈s.push(x)把 x 放到栈顶
出栈s.pop()把栈顶元素扔掉(不返回它!)
看栈顶s.top()返回栈顶元素的值(不删除)
判空s.empty()栈空返回 true
大小s.size()栈内元素个数
🚨 两个新手必踩的坑

坑 1:pop() 不会返回被删掉的元素。 这跟很多语言(比如 Python 的 list.pop())不一样。你要先 top() 看一眼, 再 pop() 删掉。这两步必须分开写:

int x = s.top();   // 先拿到值
s.pop();           // 再删掉

坑 2:栈空的时候不能 top() 也不能 pop()。 对空栈调用这两个函数是未定义行为(可能崩溃、可能读到垃圾值,也可能这次碰巧没事下次就 RE)。 铁律:动手之前先 if(!s.empty())。

实际跑一遍看看

下面这段代码我实际编译运行过,输出是真实的:

stack<int> s;
cout << "empty? " << (s.empty() ? "是" : "否") << "\n";
s.push(1); s.push(2); s.push(3);
cout << "size=" << s.size() << " top=" << s.top() << "\n";
s.pop();
cout << "pop 后 size=" << s.size() << " top=" << s.top() << "\n";
while(!s.empty()){ cout << "弹出 " << s.top() << "\n"; s.pop(); }
cout << "清空后 empty? " << (s.empty() ? "是" : "否") << "\n";

真实输出:

empty? 是
size=3 top=3
pop 后 size=2 top=2
弹出 2
弹出 1
清空后 empty? 是
✅ 看懂了什么

依次 push 了 1、2、3,栈顶是 3(最后进的)。
pop 一次以后栈顶变成 2,size 从 3 变成 2。
清空时弹出的顺序是 2、1 —— 和进来的顺序(1、2、3)完全相反。这就是「后进先出」。

顺便注意那个 while(!s.empty()):这是清空栈的标准写法。 C++ 的 stack 没有 clear() 函数(这是个常见的抱怨),只能这样一个个弹掉。

💡 用栈的时候脑子里要有画面

把栈竖着画:push 是往上叠,pop 是从上面拿走,top 是看最上面那个。 想不清楚的时候就在草稿纸上竖着写一列数字,画个箭头,比死看代码有用得多。

2队列 queue:先进先出(FIFO)

比方

食堂打饭的队伍。新来的人站在队尾,打饭的阿姨从队首叫下一个人。 先来的先走。

这就是队列:First In First Out(先进先出),简称 FIFO。

操作写法作用
入队q.push(x)把 x 放到队尾
出队q.pop()把队首元素扔掉(同样不返回值)
看队首q.front()返回队首元素(下一个轮到谁)
看队尾q.back()返回队尾元素(最后进来的那个)
判空q.empty()队列空返回 true
大小q.size()队列内元素个数
⚠️ 队列最容易记错的一点

队列是两头都能看,但只有一头能进、另一头能出:

· push 永远加在 back(队尾)
· pop 永远删掉 front(队首)

所以「front 和 back 是两个不同的元素」(除非队列里只有一个)。 刚学的时候很容易以为 push 进去马上就能 front 出来。

实际跑一遍

queue<int> q;
q.push(10); q.push(20); q.push(30);
cout << "size=" << q.size()
     << " front=" << q.front() << " back=" << q.back() << "\n";
q.pop();
cout << "pop 后 front=" << q.front()
     << " back=" << q.back() << " size=" << q.size() << "\n";

真实输出:

size=3 front=10 back=30
pop 后 front=20 back=30 size=2

依次 push 了 10、20、30。队首是 10(先进来的),队尾是 30(最后进来的)。
pop 一次以后,10 出去了,队首变成 20,队尾还是 30。先进先出,顺序和进来时一样。

🎯 队列真正的用武之地在 Day 20

今天你只是拿队列做「模拟排队」这种直白的题。但队列真正的威力在 BFS(广度优先搜索): BFS 要「一层一层往外扩」,而「先发现的先处理」正好就是队列的规则。

今天把接口练熟,到时候写 BFS 就只管想逻辑,不用再纠结语法。

3三个容器接口对照表(截图存手机)

今天还要认识第三个容器 priority_queue(优先队列)。 它不在今天的 4 道题里,但接口必须现在背下来 —— 后面贪心、最短路(Dijkstra)都会用到它。

想做的事 stack queue priority_queue
头文件<stack><queue><queue>
加入元素push(x)push(x)push(x)
删除元素pop()pop()pop()
看「下一个是谁」top()front()top()
看另一端—(没有)back()—(没有)
判空empty()empty()empty()
元素个数size()size()size()
规则后进先出 LIFO先进先出 FIFO每次出最大的
🚨 最容易记混的一格:top 还是 front?

stack 和 priority_queue 用 top(); 只有 queue 用 front()。

记法:栈和优先队列都是「一堆东西里取出最该拿的那个」→ top;队列是「排队」→ 看队首 front。

写错了编译器会直接报错('class std::queue<int>' has no member named 'top'), 不算致命,但考场上白白浪费时间。

priority_queue 默认是「大根堆」

它的规则是:不管谁先进来,出队时永远是当前最大的那个先出。 我用一段代码实测过:

// 默认写法:大根堆,top() 是最大值
priority_queue<int> pq;
for(int x : {5, 1, 9, 3, 7}) pq.push(x);
cout << "top=" << pq.top() << " size=" << pq.size() << "  -> 依次弹出:";
while(!pq.empty()){ cout << " " << pq.top(); pq.pop(); }
cout << "\n";

// 想要小根堆:加上 vector<int> 和 greater<int> 两个参数
priority_queue<int, vector<int>, greater<int>> minpq;
for(int x : {5, 1, 9, 3, 7}) minpq.push(x);
cout << "top=" << minpq.top() << "  -> 依次弹出:";
while(!minpq.empty()){ cout << " " << minpq.top(); minpq.pop(); }
cout << "\n";

真实输出:

top=9 size=5  -> 依次弹出: 9 7 5 3 1
top=1  -> 依次弹出: 1 3 5 7 9
⚠️ 小根堆的声明要写三个尖括号,一个字都不能少

priority_queue<int, vector<int>, greater<int>> minpq;

· 第 1 个参数:元素类型(int)
· 第 2 个参数:底层用哪个容器装,一般就写 vector<int>
· 第 3 个参数:怎么比较。greater<int> 表示「小的优先」→ 小根堆

注意最后那个 >> 在现代 C++ 里可以直接连着写(老编译器要求写成 > > 加空格, C++11 之后不需要了)。

三个容器都是「只能在一头或两头操作」,所以都不支持下标访问(不能写 s[0]), 也都不能用 for 循环遍历。想知道里面有什么,只能一个个 pop 出来。

4经典应用一:括号匹配

这是栈最著名的用途,也是今天 P1739 的做法。

问题

给一个只含 ( 和 ) 的表达式,判断括号是否配对。

为什么用栈

从左往右读,每遇到一个 ),它要和左边最近的那个还没配对的 ( 配对。

「最近的那个还没配对的」—— 这不就是栈顶吗?

🎯 把栈和栈顶翻译成大白话

· push 一个左括号 = 「记下来,我还欠一个右括号」
· pop 一个 = 「刚才那个左括号被配掉了,不欠了」
· 栈顶 = 「最近一个还没被配掉的左括号」
· 最后栈空 = 所有左括号都被配掉了

算法步骤

  1. 从左往右扫表达式。
  2. 遇到 ( → 入栈。
  3. 遇到 ) → 先检查栈是不是空的。如果空,说明这个右括号没有左括号配它, 立刻判定不匹配(不用再往后看了,直接输出 NO)。如果不空,就 pop 一个。
  4. 扫完之后,检查栈是否为空。如果不空,说明有左括号没被配掉 → 不匹配。
🚫 两个地方必须检查,少一个就错

① 遇到右括号时栈为空 —— 这是「右括号多了」。例子:)x+y(
② 扫完之后栈不为空 —— 这是「左括号多了」。例子:((x+y)

很多人只写了第 ② 条,遇到 )( 这种「右括号在前」的情况就会判成 YES,其实应该是 NO。 第 ① 条必须在循环里当场判掉,不能等到最后。

手推一个例子:((x+y))*

读到的字符动作栈内容(底 → 顶)
(入栈(
(入栈( (
x不管( (
+不管( (
y不管( (
)栈不空 → 弹一个(
)栈不空 → 弹一个(空)
*不管(空)
结束栈空 → 匹配→ YES
💡 其实只数个数也行(本题因为只有圆括号)

如果括号只有一种(全是大括号或全是小括号),那么用一个计数器 cnt 就够了: 遇 ( 就 cnt++,遇 ) 就 cnt--, 中途 cnt 不能变成负数,最后 cnt 必须为 0。

但一旦出现多种括号(比如同时有 () 和 []),计数器就不够用了 —— 因为 ([)] 里括号个数是配平的,可它是错的。那时必须用栈(栈里存左括号的类型,遇到右括号时检查栈顶类型是否对应)。 今天先用圆的练手,但心里要知道栈才是通用解法。

5经典应用二:后缀表达式求值

什么是后缀表达式

我们平时写的是中缀表达式:3*(5-2)+7。运算符在中间,还要考虑优先级和括号。

后缀表达式(又叫逆波兰式)把运算符写在两个操作数后面,而且不需要括号、不需要考虑优先级, 从左往右按顺序算就行。3*(5-2)+7 的后缀写法是:

3.5.2.-*7.+@

其中 . 是「一个操作数结束了」的标记,@ 是整个表达式的结束标记。

求值规则:一句话概括

🎯 遇数字入栈,遇运算符弹出两个数算完再入栈

最后栈里只剩一个数,那就是答案。

手推一遍:3.5.2.-*7.+@

读到的内容动作栈内容(底 → 顶)
3.数字入栈3
5.数字入栈3 5
2.数字入栈3 5 2
-弹出 2 和 5,算 5−2=3,入栈3 3
*弹出 3 和 3,算 3×3=9,入栈9
7.数字入栈9 7
+弹出 7 和 9,算 9+7=16,入栈16
@结束答案 = 16

走完之后栈里只剩 16,和样例输出一致。(顺便也能看出为什么不需要括号: 5−2 先算,是因为 - 写在 5 和 2 后面、排在 * 前面。)

🚨 今天最大的坑:减法和除法的操作数顺序!

遇到运算符时,我们要弹出两个数。假设先弹出来的是 b、后弹出来的是 a,那么:

a = 先压进去的(后弹出的)  // 左操作数
b = 后压进去的(先弹出的)  // 右操作数

结果 = a - b     // 一定要写成 a-b,不是 b-a!
结果 = a / b     // 除法同理

为什么? 因为栈是后进先出。在后缀表达式 5.2.- 里,5 先进栈、2 后进栈, 所以 2 在栈顶、先被弹出。而我们要算的是 5−2,也就是「先弹出的当右操作数」。

加法和乘法无所谓(交换律),但减法和除法写反了就是错的。这是 P1449 最常见的 WA 原因。

第二个坑:多位数怎么读

题目里说「数字可能是多位的」。像 10.28.30./*7.-@ 里的 10、28、30 都是两位数。所以我们不能读一个字符就当做一个数字。

🎯 正确做法:一位一位拼,直到遇到 .
long long num = 0;                 // 正在拼的数
if(c 是数字)   num = num * 10 + (c - '0');   // 拼上去
else if(c == '.')   { 入栈(num); num = 0; }  // 一个数读完了

比如读 1、0、.:num 先变 1,再变成 1×10+0 = 10,遇到 . 时入栈 10。 这跟 Day 6 学的「数字拆分」是同一个套路,只是方向反过来 —— 那边是往外拆,这边是往里拼。

⚠️ 关于除法的「向 0 取整」

题目特别说明:/ 的结果要向 0 取整,并且「与 C++ / 运算的规则一致」。

这是好消息:C++ 的整数除法天生就是向 0 取整(−7/2 = −3,7/2 = 3),所以你直接用 / 就行, 不需要任何额外处理。(这一点和其他一些语言不一样,那些语言里负数除法是向下取整。)

6四道例题详解

下面每道题的代码都用 g++ -std=c++14 -O2 -Wall 实际编译通过(零警告)并跑过官方样例,输出是你看到的真实结果。

B3614 【模板】栈 普及- · 栈的基本操作

题意:实现一个栈,支持四种操作:

  • push(x):把 x 入栈。
  • pop():弹出栈顶。如果栈是空的,不做任何操作,输出 Empty。
  • query():输出栈顶元素。如果栈是空的,输出 Anguei!。
  • size():输出栈内元素个数。
⚠️ 数据范围(原文)

本题单测试点内有多组数据。 1 ≤ T, n ≤ 10⁶,且 Σn ≤ 10⁶; 0 ≤ x < 2⁶⁴。

两个关键点:
① Σn ≤ 10⁶ —— 操作总数一百万,必须用快读/关同步,否则可能 TLE;
② x < 2⁶⁴ —— x 最大接近 1.8×10¹⁹,超过 unsigned long long 就装不下了, 必须用 unsigned long long(4 字节的 int、8 字节的 long long 里, 只有 unsigned long long 能表示到 2⁶⁴−1)。

题目自己也提醒:「请注意大量数据读入对程序效率造成的影响」。

思路

用 std::stack 直接做。要点有三个:

1. 数据类型用 unsigned long long,因为 x 可以到 2⁶⁴−1。

2. 每次 pop / query 之前都要判空,空的时候输出对应的字符串而不是去动栈。

3. 六种操作怎么区分。 这里有个陷阱:

🚨 我在写这题时真的踩了这个坑

我一开始想偷懒,用第二个字符来区分:if(op[1]=='u') 认为是 push, if(op[1]=='o') 认为是 pop。

这是错的! 因为 "query" 的第二个字符也是 'u' (q-u-e-r-y)!于是所有的 query 都被当成了 push, 程序去读一个根本不存在的整数 x,cin 进入失败状态,后面的所有读入全部失效 —— 结果就是程序陷入死循环、疯狂输出 0。

正确做法:先看第一个字符。 'p' 开头是 push 或 pop(这两个才用第二个字符区分), 'q' 开头是 query,'s' 开头是 size。

样例输入
2
5
push 2
query
size
pop
query
3
pop
query
size
样例输出
2
1
Anguei!
Empty
Anguei!
0

第一组:push 2 → query 输出 2 → size 输出 1 → pop(成功,静默)→ query 时栈已空,输出 Anguei!
第二组:栈一直是空的,pop 输出 Empty,query 输出 Anguei!,size 输出 0。

完整代码

#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false);      // 数据量大,关掉同步更快
    cin.tie(nullptr);

    int T;
    cin >> T;
    while(T--){                       // ★ 多组数据
        int n;
        cin >> n;
        stack<unsigned long long> st;  // ★ x 可达 2^64-1,必须 ull
        while(n--){
            string op;
            cin >> op;
            // 注意:push / pop 都以 'p' 开头,query 缩写成 q,size 缩写成 s
            // 千万不能用 op[1] 区分 push 和 query —— "query"[1] 也是 'u'!
            if(op[0] == 'p'){
                if(op[1] == 'u'){          // push
                    unsigned long long x;
                    cin >> x;
                    st.push(x);
                }else{                     // pop
                    if(st.empty()) cout << "Empty\n";
                    else st.pop();
                }
            }else if(op[0] == 'q'){        // query
                if(st.empty()) cout << "Anguei!\n";
                else cout << st.top() << "\n";
            }else{                         // size
                cout << st.size() << "\n";
            }
        }
    }
    return 0;
}
✅ 实测结果

官方样例 → 输出 2 / 1 / Anguei! / Empty / Anguei! / 0 完全一致

极值测试:push 18446744073709551615(就是 2⁶⁴−1,unsigned long long 能存的最大值) 然后 query → 输出 18446744073709551615,size → 1。
这验证了 unsigned long long 的选择是对的:换成长度不够的类型这里就已经错了。

易错点

  • 用 int 或 long long 存 x → x 可以到 2⁶⁴−1,存不下。必须 unsigned long long。
  • 用 op[1] 区分操作 → 会把 query 误判成 push(我实际踩过,会死循环)。要先看 op[0]。
  • 忘记判空 → 对空栈 top() 是未定义行为,可能 RE。
  • 注意两个字符串的拼写:Empty 没有感叹号,Anguei! 有感叹号。题目专门提醒过。
  • 多组数据要重新声明栈:把 stack 定义在 while(T--) 循环里面, 否则上一组残留的数据会污染下一组。
  • 不关 ios::sync_with_stdio 的话,10⁶ 次读入可能超时。
B3616 【模板】队列 普及- · 队列的基本操作

题意:实现一个队列。操作不是用字符串给的,而是用数字编号:

输入含义空队列时的行为
1 x把 x 加入队尾—
2弹出队首输出 ERR_CANNOT_POP
3查询队首输出 ERR_CANNOT_QUERY
4查询元素个数输出 0(正常输出)
⚠️ 数据范围(原文)

对于 100% 的测试数据,满足 n ≤ 10000,且被插入队列的所有元素值都是 [1, 1000000] 以内的正整数。

数据非常小,int 存元素完全够用,也不需要考虑读入效率。这题的重点是把接口用对。

思路

纯模拟题,读一个操作号,分支处理即可。三个要点:

1. 操作 1 后面还跟着一个数 x,要接着读进来。

2. 操作 2 和 3 都要先判空,空的时候输出错误提示字符串。

3. 输出格式。 题目文字里有一句「每条输出之间应当用空行隔开」, 但官方样例输出里并没有空行。

💡 遇到「题面描述」和「样例」打架时怎么办

以样例为准。评测用的就是标准输出文件,而样例就是标准输出的一部分。

我的做法是按样例输出(不加空行),实测通过样例。如果评测真的要求空行,样例不可能长这样。 但你要知道这个坑存在 —— 这是我这次唯一不太确定的点,已如实说明。

样例输入
13
1 2
3
4
1 233
3
2
3
2
4
3
2
1 144
3
样例输出
2
1
2
233
0
ERR_CANNOT_QUERY
ERR_CANNOT_POP
144

逐步走一遍(题目提示里的解释):
插入 2 → 队首是 2、个数 1 → 插入 233 → 队首还是 2(先进先出!)→ 弹出 → 队首变成 233 → 再弹出 → 队列空、个数 0 → 查询队首(空)报错 → 弹出(空)报错 → 插入 144 → 队首 144。

完整代码

#include <bits/stdc++.h>
using namespace std;

int main(){
    int n;
    cin >> n;

    queue<int> q;
    while(n--){
        int op;
        cin >> op;
        if(op == 1){
            int x;  cin >> x;              // ★ 操作 1 后面还有个数
            q.push(x);
        }else if(op == 2){
            if(q.empty()) cout << "ERR_CANNOT_POP\n";
            else q.pop();
        }else if(op == 3){
            if(q.empty()) cout << "ERR_CANNOT_QUERY\n";
            else cout << q.front() << "\n";  // ★ 队列用 front(),不是 top()
        }else{
            cout << q.size() << "\n";       // 操作 4:空队列也正常输出 0
        }
    }
    return 0;
}
✅ 实测结果

官方样例 → 输出 2 / 1 / 2 / 233 / 0 / ERR_CANNOT_QUERY / ERR_CANNOT_POP / 144 (共 8 行),与官方样例逐行完全一致。

注意这 8 行输出里没有空行,这印证了上面那条判断:以样例为准。

易错点

  • 把 front() 写成 top() → 编译报错(queue 没有 top)。
  • 操作 1 忘了读 x → 后面的读入全部错位。
  • 空队列时去 front() / pop() → 未定义行为。必须先判空。
  • 操作 4 也去判空 → 空队列时 size 应该正常输出 0,不需要任何特殊处理。
  • 两个错误字符串拼写要一字不差:ERR_CANNOT_POP 和 ERR_CANNOT_QUERY(都是大写 + 下划线)。
P1449 后缀表达式 普及- · 栈的应用

题意:给一个后缀表达式(逆波兰式),求它的值。运算符只有 + - * /。 操作数后面跟 . 表示这个数结束,整个表达式用 @ 结尾。 保证除法除数不为 0,且除法结果向 0 取整。

⚠️ 数据范围(原文)

数据保证 1 ≤ |s| ≤ 50,答案和计算过程中的每一个值的绝对值不超过 10⁹。

字符串长度只有 50,所以时间、空间都毫无压力。难点完全在于解析和运算顺序。

注意「过程中每个值的绝对值不超过 10⁹」这句话 —— 用 int 其实也放得下, 但用 long long 更保险(反正内存不差这点)。

思路

一个栈搞定,从左往右扫字符串:

  1. 读到 数字字符 → 拼到当前数字里:num = num * 10 + (c - '0')。
  2. 读到 . → 一个数读完了,把 num 入栈,然后 num = 0 重新开始。
  3. 读到 @ → 表达式结束,跳出循环。
  4. 读到 运算符 → 先弹出的是右操作数,后弹出的是左操作数,算完把结果入栈。

循环结束后,栈顶就是答案。

样例输入 1
3.5.2.-*7.+@
样例输出 1
16
样例输入 2
10.28.30./*7.-@
样例输出 2
-7

样例 2 手推:10、28、30 依次入栈 → 遇 / 弹出 30 和 28, 算 28 / 30 = 0(整数除法)→ 入栈得到 [10, 0] → 遇 * 弹出 0 和 10, 算 10 × 0 = 0 → 入栈得到 [0] → 7 入栈得到 [0, 7] → 遇 - 弹出 7 和 0, 算 0 − 7 = −7。注意这里就是「先弹出的当右操作数」:0 − 7,不是 7 − 0。

完整代码

#include <bits/stdc++.h>
using namespace std;

int main(){
    string s;
    cin >> s;

    stack<long long> st;
    long long num = 0;                // 正在拼的数字

    for(size_t i = 0; i < s.size(); i++){
        char c = s[i];
        if(c >= '0' && c <= '9'){
            num = num * 10 + (c - '0');   // ★ 多位数:一位一位拼起来
        }else if(c == '.'){
            st.push(num);                 // 遇到 '.' 说明一个数读完了
            num = 0;
        }else if(c == '@'){
            break;                        // 表达式结束
        }else{                            // 运算符
            long long b = st.top(); st.pop();   // ★ 先弹出的是右操作数
            long long a = st.top(); st.pop();
            long long r = 0;
            if(c == '+')      r = a + b;
            else if(c == '-') r = a - b;
            else if(c == '*') r = a * b;
            else              r = a / b;  // C++ 的 / 本身就是向 0 取整
            st.push(r);
        }
    }

    cout << st.top() << "\n";
    return 0;
}
✅ 实测结果

官方样例 1 3.5.2.-*7.+@ → 16 完全一致
官方样例 2 10.28.30./*7.-@ → -7 完全一致

多位数专项测试(这题的评分点就在这儿):
· 123.456.+@ → 579(123 + 456)
· 1000.1000.*2.-@ → 999998(1000×1000 − 2)
· 42.@ → 42(表达式里只有一个数)

易错点

  • 减法和除法把操作数顺序弄反 → 这是本题第一大 WA 原因。记住:先弹出的是右操作数。
  • 多位数没处理 → 读到 10. 时只识别成 1,后面全乱。必须用 num * 10 + digit 累积。
  • 读到 . 忘了把 num 清零 → 下一个数字会接在旧的后面(比如 12 之后读 3 会变成 123)。
  • 用 cin >> s 读入是可行的(表达式里没有空格),但如果题目说「一行可能带空格」就得用 getline。
  • 循环条件写成 i < s.size() 但忘了处理 @ → @ 会被当成运算符处理。要显式 break。
  • 判断数字要用 c >= '0' && c <= '9',不要用字符的 ASCII 码硬编码。
P1739 表达式括号匹配 入门 · 普及- · 栈 / 计数

题意:表达式由数字、小写字母、运算符 + - * / 和左右小括号组成,以 @ 结尾。 判断圆括号是否匹配:匹配输出 YES,否则输出 NO。

只有圆括号要管,方括号、大括号题面里没有。

⚠️ 数据范围(原文)

表达式长度小于 255,左圆括号少于 20 个。

数据极小,用栈还是用计数器都毫无压力。这题的重点是把两种情况都想全。

思路

用栈做(这是通用解法):遇 ( 入栈;遇 ) 时先判空 —— 空说明右括号多了,立刻判 NO;不空就弹一个。扫完后再判一次栈是否为空,不空说明左括号多了。

🎯 这题其实可以不用栈

因为只有一种括号,用一个计数器就够了:遇 ( 加一、遇 ) 减一。

但为了跟 Day 28 的主题呼应、也为了以后遇到「多种括号」时能直接套用, 我的代码还是用栈写的。栈的写法通用性更强,建议你练熟这一版。

样例输入 1
2*(x+y)/(1-x)@
样例输出 1
YES
样例输入 2
(25+x)*(a*(a+b+b)@
样例输出 2
NO

样例 1:两个 ( 两个 ),且顺序正确 → YES。
样例 2:有两个 ( 但只有一个 ),结束时栈里还剩一个 → NO。

完整代码

#include <bits/stdc++.h>
using namespace std;

int main(){
    string s;
    cin >> s;

    stack<char> st;
    bool ok = true;

    for(size_t i = 0; i < s.size(); i++){
        char c = s[i];
        if(c == '@') break;                 // 表达式结束
        if(c == '('){
            st.push(c);                     // 左括号入栈
        }else if(c == ')'){
            if(st.empty()){                 // ★ 右括号多了,当场判错
                ok = false;
                break;
            }
            st.pop();                       // 和栈顶的左括号配对
        }
    }

    if(!st.empty()) ok = false;             // ★ 左括号多了
    cout << (ok ? "YES" : "NO") << "\n";
    return 0;
}
✅ 实测结果

官方样例 1 2*(x+y)/(1-x)@ → YES 完全一致
官方样例 2 (25+x)*(a*(a+b+b)@ → NO 完全一致

边界测试:
· ((x+y))*@(嵌套 + 并列)→ YES
· )x+y(@(右括号在前)→ NO ← 如果只判「最后栈是否为空」,这里会错判成 YES
· x+y@(一个括号都没有)→ YES(空表达式也算匹配)

易错点

  • 只在最后判一次栈是否为空,遇到 )( 这种会错判成 YES。必须在循环里对空栈判错。
  • 对空栈调用 top() / pop() → 未定义行为。要先判空。
  • 忘记在 @ 处停下 → 后面的字符会被当成表达式内容。
  • 输出是 YES / NO,全大写,别写 Yes 或 yes。
  • 题目说的是「左圆括号少于 20 个」,所以栈的深度很小,不用担心中间会爆栈。

✗今日常见错误汇总

#错误写法后果正确写法
1 对空栈直接 top() 或 pop() 未定义行为,可能 RE、可能读到垃圾值 if(!s.empty()) 之后再动手
2 以为 pop() 会返回被删的值
int x = s.pop();
编译报错(pop 返回 void) int x = s.top(); s.pop();
3 队列用 q.top() 编译报错:queue 没有 top 队列是 q.front(),栈和优先队列才是 top()
4 后缀表达式里
r = b - a
减法和除法结果全反 → WA r = a - b;(先弹出的是右操作数)
5 后缀表达式一次只读一位数字 多位数(10、28、30)被拆散 → WA num = num*10 + (c-'0'),遇 . 才入栈
6 括号匹配只在最后判一次空栈 )(x+y( 这类会错判成 YES 遇到右括号时若栈空 → 立刻判 NO
7 B3614 用 op[1] 区分操作 "query"[1] 也是 'u',被当成 push
→ 读入失败 → 死循环
先看 op[0],再决定要不要看 op[1]
8 B3614 用 int / long long 存 x x < 2⁶⁴ 超出范围 → WA unsigned long long
9 多组数据时把 stack 定义在循环外 上一组残留元素污染下一组 定义在 while(T--) 循环体内
10 输出字符串拼错
Empty! / Anguei
答案错误 Empty(无叹号)
Anguei!(有叹号)

✓自测:你真的会了吗

第 1 题:下面这段代码错在哪?
stack<int> s;
int x = s.pop();

答:两个错。

① pop() 的返回值是 void,不能赋值给任何变量,这行直接编译不过。 想拿值要先 top()。

② 就算写成 int x = s.top();,如果此时 s 是空的,同样是未定义行为。 必须先 if(!s.empty())。

第 2 题:栈和队列都能用 push 和 pop,那怎么区分「下一个出来的是谁」?

答:

· stack:push 和 pop 都在同一头(栈顶),用 top() 看 → 后进先出。
· queue:push 进队尾、pop 从队首出,用 front() 看下一个、 back() 看最后进来的 → 先进先出。

一句话:栈是一头进出,队列是两头分工。

第 3 题:手算 5.3.-2.*@ 的值,并写出每一步的栈内容。

答:

读到动作栈
5.入栈5
3.入栈5 3
-弹 3(右)、弹 5(左),算 5−3=22
2.入栈2 2
*弹 2(右)、弹 2(左),算 2×2=44
@结束答案 4

它对应的中缀表达式是 (5−3)*2,确实是 4。

第 4 题:括号匹配里,为什么「只在最后判栈是否为空」不够?举一个反例。

答:因为那只能发现「左括号多了」,发现不了「右括号多了」。

反例:)x+y(@。第一个字符就是 ),此时栈是空的 —— 它没有左括号可配。 但如果只在最后判空,扫完后栈恰好也是空的(因为中间又补了一个 ( 和一个 )), 程序就会错误地输出 YES。正确答案是 NO。

所以必须在循环里遇到 ) 时先判空,一发现空就立刻判 NO 并 break。

第 5 题(手写代码):用栈实现「把字符串反过来输出」。
string rev(string s){
    stack<char> st;
    for(char c : s) st.push(c);        // 全部压进去
    string r;
    while(!st.empty()){                // 再一个个弹出来
        r += st.top();
        st.pop();
    }
    return r;
}

★ 因为栈是后进先出,最后一个字符最先被弹出,所以弹出来的顺序正好和压进去的顺序相反。 「反转」是栈最本质的用途 —— 括号匹配里「找最近的左括号」本质上也是在利用这个性质。

第 6 题:什么时候该用 priority_queue 而不是 queue?

答:当你需要「每次取出的不是最早进来的,而是当前最大/最小的那个」时。

比如「每次从一堆任务里挑优先级最高的执行」「Dijkstra 最短路里每次取当前距离最小的点」。 这些场景的顺序和「进来时间」无关,只和「值的大小」有关,所以要用优先队列。

默认的 priority_queue<int> 是大根堆(每次出最大值); 要小根堆得写 priority_queue<int, vector<int>, greater<int>>。

▤速查卡(截图存手机)

【stack · 后进先出 LIFO】
  #include <stack>
  stack<int> s;
  s.push(x);      // 入栈
  s.top();        // 看栈顶(★ 不删除)
  s.pop();        // 删栈顶(★ 不返回)
  s.empty();  s.size();
  // ★ 先 top 再 pop;动手前先 if(!s.empty())

【queue · 先进先出 FIFO】
  #include <queue>
  queue<int> q;
  q.push(x);      // 进队尾
  q.front();      // 看队首(★ 下一个出队的)
  q.back();       // 看队尾
  q.pop();        // 删队首
  q.empty();  q.size();

【priority_queue · 每次出最大 / 最小】
  priority_queue<int> pq;                       // 大根堆
  priority_queue<int, vector<int>, greater<int>> mp;  // 小根堆
  pq.push(x);  pq.top();  pq.pop();  pq.empty();  pq.size();

【三个容器的「看下一个」】
  stack → top()      queue → front()      priority_queue → top()
  // 都不要用下标访问,都不能 for 遍历

【括号匹配】
  for(char c : s){
      if(c == '@') break;
      if(c == '(') st.push(c);
      else if(c == ')'){
          if(st.empty()){ ok = false; break; }   // ★ 右括号多了
          st.pop();
      }
  }
  if(!st.empty()) ok = false;                    // ★ 左括号多了

【后缀表达式求值】
  long long num = 0;
  for(char c : s){
      if(isdigit(c)) num = num*10 + (c-'0');     // ★ 多位数
      else if(c == '.') { st.push(num); num = 0; }
      else if(c == '@') break;
      else {
          long long b = st.top(); st.pop();      // ★ 先弹出的是右操作数
          long long a = st.top(); st.pop();
          if(c=='+') st.push(a+b);
          if(c=='-') st.push(a-b);
          if(c=='*') st.push(a*b);
          if(c=='/') st.push(a/b);               // 向 0 取整,C++ 默认行为
      }
  }
  cout << st.top();

→明天预告 · Day 29

限时模拟赛(第一次完整比赛体验)

前面 28 天你都在按知识点一天一题地练。明天要换一种练法:给你 4 道题、3 个小时, 自己判断难度、自己分配时间。

题目是 P1067 多项式输出、P1328 生活大爆炸版石头剪刀布、P1981 表达式求值、P1996 约瑟夫问题 —— 都不难,但在压力下把简单的题写对,本身就是一项需要专门练的能力。

今天学的栈和队列明天不会直接用上,但「用容器管理数据」的思路会一直跟着你。 另外 Day 20 的 BFS 就是靠队列撑起来的 —— 今天的接口就是那时候的地基。