CSP-J 2022 · 表达式解析 · 短路求值

P8815 逻辑表达式

本节课只抓一个关键问题:表达式可以算出结果,但短路次数必须按“真正被执行的路径”统计。

长度 10^6需要线性算法
& 优先高于 | 运算
左到右同级运算顺序
跳过右边短路的核心动作

一、先回答问题

结论

先建树,再沿着会执行的路径求值

每个运算符都先算左边。只有左边不能决定答案时,才进入右边。

0 & x 一定是 0,所以 x 不会被计算
1 | x 一定是 1,所以 x 不会被计算
本课安排

把“不能统计跳过部分”讲透

先看短路规则,再用样例追踪计数变化,最后把规则落到两个栈建树和非递归求值代码中。

最容易错的地方:看到右子树里有短路,也不能马上统计。它必须真的被计算到。

二、核心观察

建模

表达式树

数字是叶子结点,`&` 和 `|` 是内部结点。整道题就是在树上求根的值。

优化

两个栈建树

运算符栈处理优先级,结点栈保存已经形成的子树。每个字符只处理常数次。

正确性

只走执行路径

求值时先进入左子树。若左值已经触发短路,就不进入右子树,也不统计右子树内部。

两个判断规则

当前运算 左边的值 动作 计数
a & b 0 答案已经是 0,跳过右边 b & 短路 +1
a & b 1 必须继续计算右边 b 不加
a | b 1 答案已经是 1,跳过右边 b | 短路 +1
a | b 0 必须继续计算右边 b 不加

三、互动演示

样例追踪

0&(1|0)|(1|1|1&0)

0&(1|0) | (1|1) | 1&0
步骤1 / 6
当前值?
& 短路0
| 短路0

老师板书

这一步到底在训练什么?

不是在数表达式里出现了多少个 0&1|

而是在模拟程序真正的执行顺序:左边先算,右边可能根本不会被看见。

在样例里,最后的 1&0 本身会短路吗?如果单独计算会。但它被外层 | 跳过,所以本题不能统计它。

四、自己选答案

问题 1

1|(0&1) 的统计结果是?

问题 2

0&1&1 有几次 & 短路?

问题 3

为什么不用递归求值作为主代码?

五、手推结果

被计算的部分 判断 当前结果 计数变化
0&(1|0) 左边是 0,右边整段跳过 0 & 加 1
0 | 右边 左边是 0,不能短路 继续计算右边 不变
1|1 左边是 1,右边跳过 1 | 加 1
(1|1) | (1&0) 左边已经是 1,右边整段跳过 1 | 再加 1
整式 0|1 = 1 1 最终 1 2

六、C++14 参考代码

两个栈建树 + 手写栈求值
#include <bits/stdc++.h>
using namespace std;

struct Node {
    char op;
    int l, r;
};

int priorityOf(char c) {
    if (c == '|') return 1;
    if (c == '&') return 2;
    return 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string s;
    cin >> s;

    vector<Node> tree;
    vector<int> nodes;
    vector<char> ops;
    tree.reserve(s.size() + 5);
    nodes.reserve(s.size() + 5);
    ops.reserve(s.size() + 5);

    auto newNode = [&](char op, int l = -1, int r = -1) {
        tree.push_back({op, l, r});
        return (int)tree.size() - 1;
    };

    auto build = [&](char op) {
        int r = nodes.back();
        nodes.pop_back();
        int l = nodes.back();
        nodes.pop_back();
        nodes.push_back(newNode(op, l, r));
    };

    for (char c : s) {
        if (c == '0' || c == '1') {
            nodes.push_back(newNode(c));
        } else if (c == '(') {
            ops.push_back(c);
        } else if (c == ')') {
            while (!ops.empty() && ops.back() != '(') {
                char op = ops.back();
                ops.pop_back();
                build(op);
            }
            ops.pop_back();
        } else {
            while (!ops.empty() && ops.back() != '(' &&
                   priorityOf(ops.back()) >= priorityOf(c)) {
                char op = ops.back();
                ops.pop_back();
                build(op);
            }
            ops.push_back(c);
        }
    }

    while (!ops.empty()) {
        char op = ops.back();
        ops.pop_back();
        build(op);
    }

    int root = nodes.back();
    vector<unsigned char> val(tree.size(), 0);
    long long cntAnd = 0, cntOr = 0;

    struct Frame {
        int u;
        int state; // 0: 先算左子树,1: 判断是否短路,2: 已算右子树
    };

    vector<Frame> st;
    st.reserve(tree.size());
    st.push_back({root, 0});

    while (!st.empty()) {
        Frame &f = st.back();
        Node &cur = tree[f.u];

        if (cur.op == '0' || cur.op == '1') {
            val[f.u] = cur.op - '0';
            st.pop_back();
        } else if (f.state == 0) {
            f.state = 1;
            st.push_back({cur.l, 0});
        } else if (f.state == 1) {
            int leftValue = val[cur.l];
            if (cur.op == '&' && leftValue == 0) {
                cntAnd++;
                val[f.u] = 0;
                st.pop_back();
            } else if (cur.op == '|' && leftValue == 1) {
                cntOr++;
                val[f.u] = 1;
                st.pop_back();
            } else {
                f.state = 2;
                st.push_back({cur.r, 0});
            }
        } else {
            if (cur.op == '&') {
                val[f.u] = val[cur.l] & val[cur.r];
            } else {
                val[f.u] = val[cur.l] | val[cur.r];
            }
            st.pop_back();
        }
    }

    cout << (int)val[root] << '\n';
    cout << cntAnd << ' ' << cntOr << '\n';

    return 0;
}
读代码 1

建树时弹出旧运算符

priorityOf(ops.back()) >= priorityOf(c) 保证同级从左到右,且 &| 先结合。

读代码 2

短路时不压右子树

只要不把右子树压入求值栈,它内部所有短路自然都不会被统计。

七、易错点收束

错误 1

数所有局部短路

外层已经跳过的部分不能统计。先问“它有没有被执行到”。

错误 2

忘记优先级

0|1&0 等价于 0|(1&0),不是 (0|1)&0

错误 3

递归爆栈

本题长度到 10^6,课堂上可以讲递归思想,但提交代码更稳的是手写栈。