← 目录 第 29 / 60 节 全部 60 节

29 · 栈与队列的应用

把 LIFO/FIFO 从“系统自动帮你维护”变成“自己调度”:后缀表达式求值(栈顶即最近的两个操作数)、括号匹配(栈顶即最近未匹配的左括号)、递归改显式栈、队列模拟约瑟夫环。
普及/提高-预计 60 分钟栈括号匹配后缀表达式
本节出现的新函数 / 新语法(先看懂再用)

栈和队列不是两个新容器,是两种顺序约定:栈规定“最后进来的第一个出去”(LIFO),队列规定“最先进来的第一个出去”(FIFO)。前面几节其实一直在用它们 ——BFS 用队列按层扩散,递归用系统栈保存现场 —— 但都是自动的。这一节把它们拿到手里,用一行 st.push(x) 自己决定什么时候存、什么时候取。

具体做三件事:①后缀表达式求值,看清“栈顶就是最近的两个操作数”;②括号匹配,看清“最近未匹配的左括号就是栈顶”;③把递归改写成显式栈,把队列当成一个循环的圈。

栈的语义只有一句:最近的东西最相关。凡是“最近的还没处理完”能刻画问题的地方,都能用栈。
容器从哪进 / 从哪出看一眼拿到谁典型场景
stack<int>都从栈顶最近压入的括号匹配、后缀表达式、递归改迭代、单调栈
queue<int>队尾进、队头出最早进来的BFS 按层扩散、约瑟夫环、任务排队
deque<int>两端都能进出最早或最近的0-1 BFS、滑动窗口最值(第 28 节)

三种容器的接口几乎一样(push / pop / empty / size),区别只在 pop() 删的是哪一个,以及取元素用的是 top() 还是 front()。把这三个名字对错位置,是本节第一个坎。

新函数拆解 · stack:压栈、弹栈、看栈顶
stack<int> st; st.push(x); st.top(); st.pop(); st.empty(); st.size();
组成含义 / 格式要求
stack<int>头文件 <stack>(bits 已包含)。只有“栈顶”一个出入口;底层默认用 deque 装,所以不用预先开大小。
st.push(x)压栈,O(1)。x 成为新的栈顶,原来的栈顶被压在它下面。
st.top()读栈顶,O(1),不删除。返回引用,可以赋值(st.top() += 3 合法)。栈空时调用是未定义行为,通常 RE。
st.pop()弹栈,O(1),删除栈顶,返回 void。想用被删的值,必须先 top() 存下来。
st.empty() / st.size()判空 / 元素个数。size() 的返回类型是 size_t(无符号),空栈时算 size() - 1 会得到 18446744073709551615。
没有迭代器、没有 clear()stack 故意不给你遍历和下标 —— 只能看栈顶,这就是它的语义。清空只能循环 pop,或者 st = stack<int>(); 重新赋值。
最小可运行示例
示例
stack<int> st;
st.push(1);                         // 栈底 → [1]
st.push(2);                         //        [1, 2]
st.push(3);                         //        [1, 2, 3] ← 栈顶
cout << st.top();                   // 3
st.pop();                           // 弹出 3,剩下 [1, 2]
cout << st.top() << st.size();      // 22

// 清空的标准写法
while (!st.empty()) st.pop();
注意

int x = st.pop(); 编译不过:pop() 返回 void。这是初学者最高频的一行错。

top() 返回的是引用,char c = st.top(); st.pop(); 的顺序不能反 —— 先 pop 再 top 拿到的是下一个元素。

新函数拆解 · queue 的三个操作(对照 stack 记,第 26 节已用过)
queue<int> q; q.push(x); q.front(); q.back(); q.pop(); q.empty(); q.size();
组成含义 / 格式要求
queue<int>头文件 <queue>。队尾进、队头出;先进先出,所以最早进来的最先被处理。
q.push(x)入队,O(1)。只能从队尾进。
q.front()读队头,O(1),不删除。相当于 stack 的 top(),但拿到的是最旧的元素。
q.back()读队尾,O(1)。拿到的是最新入队的元素。
q.pop()删除队头,O(1),同样返回 void。
与 stack 的对照取元素:stack 用 top(),queue 用 front()。pop() 删的都是“下一个该被处理的”,但 stack 处理最新的,queue 处理最旧的 —— 这一句就是 LIFO 与 FIFO 的全部差别。
最小可运行示例
示例
queue<int> q;
for (int i = 1; i <= 5; ++i) q.push(i);   // [1, 2, 3, 4, 5]
cout << q.front() << q.back();            // 15

// 把队头搬到队尾:整圈轮转一格
q.push(q.front());
q.pop();
cout << q.front();                        // 2
注意

第 26 节已经用 queue 写过 BFS,这里只做对照:把 top 和 front、把“最新”和“最旧”彻底分清。

queue 不能随机访问,也不能倒着走。需要两端都能动时用 deque(第 28 节)。

把队头搬到队尾只要两行:q.push(q.front()); q.pop(); —— 记住这个动作,约瑟夫环就是它。

例题 · 后缀表达式洛谷 P1449普及-去洛谷做这题 ↗

后缀表达式(逆波兰式)把运算符写在两个操作数之后。例如 3.5.2.-*7.+@ 对应中缀写法 3×(5−2)+7,值为 16。

输入一行后缀表达式:操作数是十进制非负整数,每个数后面跟一个 '.' 表示这个数结束;运算符只有 +、−、×、÷;整行以 '@' 结尾。

输出格式:一行一个整数,即表达式的值。

题目保证表达式合法;除法按 C++ 的整数除法计算(向零取整)。

输入: 3.5.2.-*7.+@ 输出: 16
为什么后缀表达式一遍就能算完

中缀表达式难算,是因为读到 + 的时候你还不知道后面有没有 ×:优先级和括号都要“往后看”。后缀表达式把这些决定提前做完了,读到运算符时,它要的两个操作数一定刚好是最近的两个。

“最近的”就是栈顶。于是算法只有三行:数字入栈;运算符弹出两个算一次再压回去;扫完栈里剩一个数。

顺序是关键。先弹出的是右操作数 b,后弹出的是左操作数 a,计算的是 a op b。对 + 和 × 无所谓,对 − 和 ÷ 写反就得到相反数或倒数 —— 这是本节最高频的 WA。

为什么这样是对的。用归纳看不变量:扫过某个前缀之后,栈里从底到顶正好是“这个前缀算出的、还没被合并的中间结果”,并且它们的相对顺序与表达式剩余部分一致。读到一个数:它是新的中间结果,压栈,不变量成立。读到一个运算符:它作用在最近的两个中间结果上,弹两个压一个,正好是它们的合并,不变量仍成立。扫完整个表达式只剩一个中间结果,就是答案。

复杂度 O(n):每个字符进栈一次或做一次常数运算。长度 10⁵ 的表达式,几百微秒的事。

四个分支,一个栈
// 骨架:一个栈包打天下(st 是 stack<long long>,num 是正在攒的数)
while (cin >> c) {
    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();  // 后弹出的是左操作数
        st.push(c == '+' ? a + b : c == '-' ? a - b : c == '*' ? a * b : a / b);
    }
}
完整可编译代码(C++17)
#include <bits/stdc++.h>
using namespace std;

int main() {
    stack<long long> st;          // 中间结果可能很大,用 long long 不用 int
    long long num = 0;            // 正在读的那个数
    char c;
    while (cin >> c) {            // 逐字符读,cin >> char 会自动跳过空白
        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;       // 不能写成 b - a
            else if (c == '*') r = a * b;
            else               r = a / b;       // 同上,顺序错了结果就反了
            st.push(r);           // 结果必须压回栈,后面的运算符要用它
        }
    }
    cout << st.top() << '\n';     // 扫完栈里恰好剩一个数
    return 0;
}
逐行拆解
代码片段含义与理由
stack<long long> st;栈里存的是“中间结果”,不是字符。用 long long 是因为 7×8×9×… 这类连乘很快超出 int,本题数据小,但换个数据就爆。
while (cin >> c)逐字符读。cin >> char 会自动跳过空格和换行,所以表达式里有没有空格都不影响。另一种写法是 getline 整行再遍历,那样要自己处理 '\r'。
num = num * 10 + (c - '0');多位数拼装。342 是 3→34→342:每次先把已有的数乘 10 进位,再加上新的一位。(c - '0') 把字符 '7' 转成整数 7,靠的是 ASCII 码连续。
else if (c == '.')本题的 '.' 是数字结束符,不是小数点。看到它就说明前面攒的 num 是一个完整的操作数。
long long b = st.top(); st.pop();先弹出的是右操作数。后缀表达式 3.5.- 的含义是 3 − 5,栈里是 [3, 5],先弹出 5 才是被减数那一侧的 b。
long long a = st.top(); st.pop();后弹出的是左操作数。两个 pop 的先后顺序就是本题唯一需要动脑的地方。
r = a - b; r = a / b;减法和除法不满足交换律,写成 b − a 后 5.3.- 会输出 −2 而不是 2。加法和乘法写反了看不出来,所以这个错常被样例放过。
st.push(r);结果必须压回栈顶,它是后续运算的操作数。漏了这一行,下一个运算符弹到的是更早的数据,整片错。
cout << st.top()合法表达式扫完后,所有中间结果都合并成了一个。若栈里还剩两个,说明表达式不合法或漏算了。
同一个栈,换个问题:括号匹配

给一行字符串(可能含其他字符,忽略),判断其中 ()、[]、{} 三种括号是否配对且嵌套正确。

栈顶就是“最近的、还没被匹配的左括号”。遇到左括号压栈;遇到右括号,它必须和栈顶配对 ——因为括号的配对是后开的先闭,这正是 LIFO。扫描结束时栈必须为空,否则有左括号没闭上。

括号匹配:栈的三个出口
#include <bits/stdc++.h>
using namespace std;

bool check(const string &s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            st.push(c);                       // 左括号:记住它,等待配对
        } else if (c == ')' || c == ']' || c == '}') {
            if (st.empty()) return false;     // 右括号多了:没有左括号可以配
            char t = st.top();
            if ((c == ')' && t != '(') ||
                (c == ']' && t != '[') ||
                (c == '}' && t != '{')) return false;   // 类型不对,嵌套关系被破坏
            st.pop();                          // 配对成功,弹出这一个
        }
    }
    return st.empty();                        // 左括号多了时这里返回 false
}

int main() {
    string s;
    getline(cin, s);
    cout << (check(s) ? "YES" : "NO") << '\n';
    return 0;
}

三个返回点对应三种失败:①栈空却来了右括号(右括号多);②类型不匹配(嵌套错);③扫完栈非空(左括号多)。漏掉任何一个,都会把非法串判成合法。

只有一种括号时,其实一个计数器就够了:遇 ( 加一、遇 ) 减一,中途不能为负、最后必须为 0。这是同一个不变式的退化版本。一旦有第二种括号,计数器就不够了 —— 它记不住“该闭的是哪一种”,例如 ([)] 的计数完全合法,但嵌套是错的。

第三个变体:出栈顺序合法吗

按 1, 2, …, n 的顺序压栈,任意时刻都可以弹栈。给定一个出栈序列,问它是否合法。判断规则只有一条:按入栈顺序逐个压栈,每压一个就检查栈顶是不是出栈序列当前要的元素,是就一直弹;压完全部之后若栈为空,就合法。

为什么这一条规则够:出栈序列的第一个元素是 k,意味着 1…k 都已经压进来过(不压进去弹不出来),而 1…k−1 还压在 k 下面。此时“能弹就弹”不会损失任何选择 —— 早弹掉一个已经可以弹的元素,只是把它从栈顶移走,不改变剩下元素之间的压弹关系。

8 行判出栈序列(3 1 2 判 false,2 1 3 判 true)
// 入栈序列固定为 1..n,判断 out[1..n] 是不是合法的出栈序列
bool ok(int n, const vector<int> &out) {
    stack<int> st;
    int p = 1;                              // 下一个还没压进去的数
    for (int i = 1; i <= n; ++i) {
        // 栈顶不是 out[i],就只能继续压,直到压出它或压完
        while (p <= n && (st.empty() || st.top() != out[i])) st.push(p++);
        if (st.empty() || st.top() != out[i]) return false;   // 压完了还是没有
        st.pop();                           // 栈顶正好是 out[i],弹掉它
    }
    return true;
}
把递归搬到堆上:用栈模拟递归

第 21 节说过:递归靠系统栈,每层压入参数、局部变量和返回地址,深度到 10⁶ 就爆栈 RE。既然栈是显式的概念,就可以自己开一个栈放到堆上,把递归改写成循环 —— 求连通块这种“只关心访问过哪些点、不关心访问顺序”的问题,改完和递归完全等价。

显式栈版 flood fill
// 网格连通块:从 (sx, sy) 出发,把能走到的 '.' 全部标记
const int N = 1005;
int n, m;
char g[N][N];
bool vis[N][N];
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};

void floodStack(int sx, int sy) {
    stack<pair<int, int>> st;
    st.push({sx, sy});
    vis[sx][sy] = true;               // ★ 入栈的同时就标记
    while (!st.empty()) {
        auto [x, y] = st.top();       // C++17 结构化绑定
        st.pop();
        for (int k = 0; k < 4; ++k) {
            int nx = x + dx[k], ny = y + dy[k];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
            if (g[nx][ny] != '.' || vis[nx][ny]) continue;
            vis[nx][ny] = true;       // ★ 这里也一样:push 之前标记
            st.push({nx, ny});
        }
    }
}

标记时机是唯一容易错的地方。递归版是“进函数就标记”,显式栈必须在 push 之前标记。如果等 pop 的时候才标记,同一个点会被它的多个邻居各压一次,在 10⁶ 的网格上栈会膨胀到千万级 → MLE 或 TLE。

两者的差别只在访问顺序:递归深度优先地一条路走到黑,显式栈的弹出顺序受入栈顺序影响,遍历次序不同但访问集合相同。所以连通性、可达性这类问题可以随意替换;要求“按字典序输出第一条路径”时不能随便换。

把队列当成一个圈:约瑟夫问题

n 个人围成圈编号 1…n,从 1 号开始报数,报到 m 的人出列,下一个人重新从 1 报,求出列顺序。

用队列,队头就是“当前该报数的人”。报 1 到 m−1 的人安全,把他从队头搬到队尾(相当于绕了一圈);轮到第 m 个人就出队并输出。“搬队头到队尾”这一个动作,就把线性队列变成了环。

队列模拟约瑟夫环
#include <bits/stdc++.h>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    queue<int> q;
    for (int i = 1; i <= n; ++i) q.push(i);

    while (!q.empty()) {
        // 前 m-1 个人安全:逐个搬到队尾,报数指针自然前移
        for (int k = 1; k < m; ++k) {
            q.push(q.front());
            q.pop();
        }
        cout << q.front() << ' ';      // 第 m 个人出列
        q.pop();
    }
    cout << '\n';
    return 0;
}

n = 10⁴、m = 10⁴ 时最坏要搬 10⁸ 次,勉强能过;n 到 10⁵ 就必须换算法。一个立刻见效的常数优化:当前还剩 s 个人时,搬 s 次就回到原点,所以只搬 (m - 1) % s 次。彻底的做法是把 m 从一开始就模掉,见下面的数学补给。

这一节的 8 个坑,每个都有具体症状

空栈上调用 top() 或 pop():未定义行为,几乎必然 RE。后缀表达式里运算符比操作数多一个就会触发;括号匹配里 )abc 这种以右括号开头的串,不先判 empty() 就是 RE。

后缀表达式两个操作数顺序搞反(a - b 写成 b - a):样例 3.5.2.-*7.+@ 会输出 −2 而不是 16,属于“样例就能查出来”的错;但很多人只测了全是 + 的数据 → WA。

运算符算完忘记 st.push(r):下一个运算符弹出的是更早的数据或空栈 → WA 或 RE。

把 '.' 当成小数点,用 stod 读它:本题的 '.' 是数字结束符,3.5. 是“数 3、数 5”两个操作数,不是小数三点五 → WA。

括号匹配只用计数法(遇左加一、遇右减一):([)] 会被判成合法 → WA。计数法只对单一类型括号成立。

括号匹配最后忘了 return st.empty():(() 被判成合法 → WA。

约瑟夫轮转次数差一:for (int k = 1; k < m; ++k) 转 m−1 次是对的,写成 k <= m 会多转一个人,n = 5、m = 2 时第一个出列的人从 2 变成 3,整串输出全错 → WA。

显式栈 DFS 在 pop 之后才标记 visited:同一点被多个邻居重复压栈,10⁶ 网格直接 MLE / TLE。

自己动手复现

关掉页面,把 P1449 的代码从零敲一遍并提交,确认样例输出 16。

改一改①:把 r = a - b; 改成 r = b - a;,用表达式 5.3.-@ 测,正确结果是 2,改完变成 −2。亲手确认一次“先弹出的是右操作数”。

改一改②:括号匹配把最后的 return st.empty() 改成 return true,用 (() 测,答案从 NO 变成 YES —— 一个左括号没闭上就被放过了。

改一改③:约瑟夫把 for (int k = 1; k < m; ++k) 改成 k <= m,跑 n = 5、m = 2:正确顺序是 2 4 1 5 3,改完第一个数就变成 3。

数学补给 · 从 O(nm) 到 O(n):约瑟夫的递推公式

队列模拟每次出列要搬 m 次,n 个人总共 O(nm)。n = m = 10⁵ 时是 10¹⁰ 次操作,必然超时。但这个环有一个只看“人数”的递推结构。

把编号改成 0 起:n 个人编号 0…n−1,报到 m 的人出列(下标是 (m−1) mod n)。出列后剩下 n−1 人,从原来编号 m mod n 的那个人开始重新报 1。把这 n−1 个人按新的起点重新编号 0…n−2,则新编号 k 对应原编号 (k + m) mod n。

于是得到递推:J(1) = 0,J(n) = (J(n−1) + m) mod n,其中 J 是胜者在新编号下的下标。答案就是 J(n) + 1(换回 1 起)。因为每次只在“少一个人”的同一个问题上加一步,复杂度 O(n)、空间 O(1)。

nJ(n)(0 起,m = 2)场上还剩的人(1 起)
101
201
323
401
523

用模拟验证 n = 5、m = 2:出列顺序 2 4 1 5 3,最后剩 3 ✓,与递推 J(5) = 2 → 3 一致。手算一遍就能确认公式没有差一:J 用的是 0 起,输出要加 1。

注意这个公式只能求最后的胜者。要输出完整的出列顺序(如 P1996),递推帮不上忙,只能模拟,或者用树状数组/线段树在 O(n log n) 内定位每次出列的人 —— 那是后面的内容。

顺手记一串数:合法括号序列的个数是卡特兰数 Catalan(n) = C(2n, n)/(n+1)(第 22 节算过)。n = 3 时是 5 种:((()))、(()())、(())()、()(())、()()()。同一串数还在数别的东西 ——n 个数进出栈的合法出栈序列也正好是 Catalan(n) 个:n = 3 时 5 个(312 不合法),n = 4 时 14 个,穷举一遍就能验证。括号匹配的栈深度最大值,就等于序列的最大嵌套层数。

举一反三 · 想清楚这些再往下走
  1. 后缀表达式如果出现一元负号(比如 -3.4.+@),现在的代码会怎样?要支持它,需要在读数字时额外维护什么状态?
  2. 中缀表达式转后缀(调度场算法)需要两个栈:一个存数字、一个存运算符。为什么运算符栈的弹出规则是“栈顶优先级 ≥ 当前优先级就弹”,而不是“只看栈顶是否相同”?
  3. 括号匹配如果要求输出“最长的合法括号子串长度”,栈还够用吗?提示:栈里存下标而不是字符,未匹配的位置就是分割点。
  4. 约瑟夫问题用队列模拟是 O(nm)。如果 n = 10⁵、m = 10⁵,而只需要最后的胜者,用数学补给的递推能到多快?如果既要胜者又要中途某个时刻的剩余人数呢?
  5. 递归改显式栈时,哪些问题可以随便换、哪些不行?以“求连通块大小”和“输出字典序最小的路径”为例各说一句。
  6. 单调栈为什么能在线性时间里求出“每个元素右边第一个比它大的数”?它保证的栈内不变式是什么?先想想:为什么被弹出元素的那个“右边第一个更大”就是当前这个新元素。
  7. 用两个栈能不能实现一个队列?反过来,用两个队列能不能实现一个栈?各自需要几次搬移,均摊复杂度是多少?
自测:能全部回答再进下一节
挑战题 · 表达式求值
洛谷 P1981普及/提高-去洛谷做这题 ↗

给定一个只含加法和乘法、不含括号的表达式,运算符按通常的优先级计算(先乘后加),求表达式的值,输出它除以 10000 的余数。

输入格式:一行字符串,为所求表达式。数字是非负整数、不含前导零,只出现 '+' 和 '*'。

输出格式:一行一个整数,表示表达式的值 mod 10000。

数据范围:表达式长度在 10⁵ 量级,单个数字可能有多位。

引导性提示(先只看这里)

从左到右直接算是不行的:1+1*3 从左结合会得 6,正确是 4。必须先算乘法。

整个表达式一定能唯一拆成若干个“加项”之和,每个加项是若干数字的连乘。想办法用一个栈保存“已经定型的加项”,再想乘法该作用在谁身上。

读到 '+' 就结算当前项、开一个新项;读到 '*' 就把当前的数直接乘到栈顶那一项上。结尾别忘了还有最后一个数没有结算。

想看思路了?点开完整思路

结构。任何只含 + 和 * 的表达式都能写成 a₁ + a₂ + … + a_k,其中每个 aᵢ 是若干数的连乘(先乘后加,加法是最后一步)。

用一个栈存加项。变量 num 表示正在读的数,op 表示上一个读到的运算符(初始设为 '+',这样第一个数会以一个加项的形式入栈)。

读到运算符 c 时:先按上一个运算符 op 结算 num —— op 是 '+' 就把 num 作为新加项 push;op 是 '*' 就把它乘进栈顶那一项(乘法优先级高,它和栈顶属于同一个加项)。然后把 op 换成 c、num 清零。先结算、再换状态,顺序不能反。

结尾:字符串末尾没有运算符,循环里不会结算最后一个 num,必须再按 op 结算一次 ——漏掉这一步会少加最后一项。

取模。只要后四位,所以每次结算都 mod 10000;加法和乘法对取模是同余的,边算边模与最后取模结果相同,但避免了天文数字溢出。

正确性。栈顶始终是“正在构造的那个加项”,遇到 '+' 就封口并开新的,遇到 '*' 就往里乘。这正是“先乘后加”的栈化,等价于中缀转后缀后再求值。复杂度 O(长度)。

还是不会?点开题解代码与拆解
参考代码
#include <bits/stdc++.h>
using namespace std;

const int MOD = 10000;

int main() {
    string s;
    getline(cin, s);

    stack<long long> st;      // 存已经定型的加项,栈顶 = 正在构造的那一项
    long long num = 0;        // 正在读的数(只保留后四位)
    char op = '+';            // 上一个运算符;初值 '+' 让第一个数以加项身份入栈

    for (size_t i = 0; i < s.size(); ++i) {
        char c = s[i];
        if (c >= '0' && c <= '9') {
            num = (num * 10 + (c - '0')) % MOD;   // 只保留后四位,等价于最后取模
        } else if (c == '+' || c == '*') {
            // 先按上一个运算符结算 num,再把 op 换成当前字符
            if (op == '+') st.push(num);          // 新的加项
            else st.top() = st.top() * num % MOD; // 上一个运算符是乘:并进栈顶那一项
            op = c;
            num = 0;
        }
        // 其他字符(空格、'\r')直接忽略:不让它们被当成运算符
    }

    // 结尾的最后一个数还没有结算
    if (op == '+') st.push(num);
    else st.top() = st.top() * num % MOD;

    long long ans = 0;
    while (!st.empty()) {                     // 把所有加项加起来
        ans = (ans + st.top()) % MOD;
        st.pop();
    }
    cout << ans % MOD << '\n';
    return 0;
}
关键片段拆解
片段作用
char op = '+';虚拟一个“上一个运算符是加号”,于是第一个数会被当成一个新加项 push。少了这个初值,第一个数要么丢失、要么要单独写一段特判。
num = (num * 10 + (c - '0')) % MOD;多位数拼装 + 随时取模。因为只要后四位,(a × 10 + d) mod 10000 = ((a mod 10000) × 10 + d) mod 10000,所以可以边读边模,不必担心数字有几百位。
if (op == '+') st.push(num);上一个运算符是加号,说明 num 是一个独立的加项,压栈。
else st.top() = st.top() * num % MOD;上一个运算符是乘号,说明 num 属于栈顶那一项,直接乘进去。top() 返回引用,所以可以这样赋值;换成 long long t = st.top(); 再 push 就错了 ——那会多出一个加项。
op = c; num = 0;结算完才更新状态,顺序不能反。先清零再结算,num 就永远是 0。
else if (c == '+' || c == '*')只认这两个字符。用 else 兜底会把行末的 '\r' 或空格当成运算符,在某些评测环境下直接算错。
if (op == '+') st.push(num); // 循环之外字符串末尾没有运算符触发结算,最后一个 num 必须手工处理。这是本题最容易漏的一块,样例 1+1*3+4 漏了会输出 4 而不是 8。
ans = (ans + st.top()) % MOD;最后求和也要取模。栈里每项都小于 10000、项数最多 10⁵ 量级,总和不超过 10⁹,int 也勉强装得下;但写成 long long 并且边加边模是零成本的保险。
这题最容易错的地方
  • 从左到右直接算:1+1*3 得 6(正确 4),必须先乘后加。
  • 漏掉结尾的最后一个数(不结算栈外的 num):1+1*3+4 输出 4 而不是 8 → WA。
  • 用 else 兜底处理运算符,把行末的 '\r' 或空格当成了运算符:轻则多出一次结算,重则 num 被错误压栈 → WA。
  • 把整条表达式的值全算出来最后才取模:数值迅速超出 long long,溢出成负数 → WA。
  • 遇到 '*' 时新开一项(push 而不是乘到栈顶):2*3*4 会变成 2+3+4 = 9,正确是 24 → WA。
延伸阅读 / 参考