先用调度场算法把中缀表达式转成后缀,再由后缀建出语法树,最后在树上按短路语义 dfs 求值,只统计真正访问到的子树里的短路次数。
OJ: luogu
题目 ID: P8815
难度:普及+/提高-
标签:栈字符串模拟
创建: 2026-06-19 20:55
更新: 2026-10-07 12:15
题意
给出一个只包含 0、1、&、
需要输出:
- 表达式最终的值
- 形如
a&b的短路次数 - 形如
的短路次数
题目规定括号优先,& 高于
思路
最直接的办法是按文法递归下降解析,并在解析过程中按短路语义求值。
先看一个可以直接验证想法的朴素解:
#include <bits/stdc++.h>
using namespace std;
struct Result {
int value;
long long short_and;
long long short_or;
};
static string s;
static int pos_idx;
Result parse_or();
void skip_or();
Result parse_factor() {
if (s[pos_idx] == '0' || s[pos_idx] == '1') {
int value = s[pos_idx] - '0';
++pos_idx;
return {value, 0, 0};
}
++pos_idx; // skip '('
Result res = parse_or();
++pos_idx; // skip ')'
return res;
}
void skip_factor() {
if (s[pos_idx] == '0' || s[pos_idx] == '1') {
++pos_idx;
return;
}
++pos_idx; // skip '('
skip_or();
++pos_idx; // skip ')'
}
Result parse_and() {
Result left_res = parse_factor();
while (pos_idx < (int)s.size() && s[pos_idx] == '&') {
++pos_idx;
if (left_res.value == 0) {
++left_res.short_and;
skip_factor();
} else {
Result right_res = parse_factor();
left_res.value &= right_res.value;
left_res.short_and += right_res.short_and;
left_res.short_or += right_res.short_or;
}
}
return left_res;
}
void skip_and() {
skip_factor();
while (pos_idx < (int)s.size() && s[pos_idx] == '&') {
++pos_idx;
skip_factor();
}
}
Result parse_or() {
Result left_res = parse_and();
while (pos_idx < (int)s.size() && s[pos_idx] == '|') {
++pos_idx;
if (left_res.value == 1) {
++left_res.short_or;
skip_and();
} else {
Result right_res = parse_and();
left_res.value |= right_res.value;
left_res.short_and += right_res.short_and;
left_res.short_or += right_res.short_or;
}
}
return left_res;
}
void skip_or() {
skip_and();
while (pos_idx < (int)s.size() && s[pos_idx] == '|') {
++pos_idx;
skip_and();
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
pos_idx = 0;
Result ans = parse_or();
cout << ans.value << '\n';
cout << ans.short_and << ' ' << ans.short_or << '\n';
return 0;
}brute.cpp 用
但表达式长度能到
样例语法树
这张图展示样例
graph TD A["|"] --> B["&"] A --> C["|"] B --> D["0"] B --> E["(1|0)"] C --> F["|"] C --> G["1&0"] F --> H["1"] F --> I["1"]
从图里可以看到,短路求值本质上就是“先看左儿子,再决定要不要进右儿子”。
例如左边的 0,就能直接确定整棵 & 子树结果为 0,右边整块都不会被访问。
因此右子树内部的任何短路,也都不该被统计。
所以正式解分成三步:
- 用调度场算法把中缀表达式转成后缀表达式(后缀里不含括号)
- 扫描后缀表达式建出语法树
- 在语法树上 dfs 后根遍历,边求值边统计短路次数
第三步是后根遍历:先递归算左子树,拿到左值后才能判断本节点会不会短路:
- 遇到
&且左值为0,统计一次&短路,本节点值为0,并且不递归右子树 - 遇到
且左值为 1,统计一次短路,本节点值为 1,同样跳过右子树 - 否则递归右子树,最后用左右儿子的值算出本节点的值
短路时不递归右子树,右子树内部的短路自然不会被统计,正满足“被外层短路的整块不计入统计”。
表达式退化成一条链(如 1|1|1|...)时递归深度会到
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-08-09 06:46
* update_at: 2026-10-05 15:03
*/
// main.cpp:P8815 逻辑表达式,正式解。
// 分三步完成:
// 1. 用调度场算法把中缀表达式转成后缀表达式(后缀里没有括号)
// 2. 扫描后缀表达式,用一个栈建出表达式树
// 3. 在表达式树上 dfs 后根遍历,按短路语义求值,同时统计短路次数
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MAXL = 1000000 + 5;
string s; // 输入的中缀表达式
string postfix; // 转换得到的后缀表达式
// 表达式树节点,struct 只放数据,一个节点的信息聚合在一起
struct Node {
char type; // 叶子是 '0'/'1',内部节点是 '&'/'|'(只有 4 种,用 char 省内存)
ll left; // 左儿子编号,叶子为 0
ll right; // 右儿子编号,叶子为 0
ll value; // 节点的值,只会是 0 或 1
};
// 动态化静态:不用指针,用静态大数组 + node_cnt 当分配指针,
// new_node 每次“开点”就把 node_cnt 加一,返回新节点编号。
Node node[MAXL]; // 表达式树,节点 u 就是 node[u]
ll node_cnt; // 已经创建的节点个数,最后一个节点是 node[node_cnt]
ll short_and; // '&' 的短路次数
ll short_or; // '|' 的短路次数
// 判断是不是运算符(本题只有两种)
bool is_op(char ch) {
return ch == '&' || ch == '|';
}
// 运算符优先级,数字越大越先算;'(' 不参与比较,固定返回 0
ll priority_of(char ch) {
if (ch == '|') {
return 1;
}
if (ch == '&') {
return 2;
}
return 0;
}
// 第一步:调度场算法,中缀表达式 -> 后缀表达式。
// 数字直接追加到结果里;运算符要入栈时,先把栈顶那些“应该先算”的弹出来。
// 因为本题同级都是从左到右算,所以栈顶优先级 >= 当前优先级时就要弹出。
string infix_to_postfix() {
string result;
stack<char> op_stack; // 调度场算法的运算符栈,存放运算符和 '('
for (ll i = 0; i < (ll)s.size(); i++) {
char ch = s[i];
if (!is_op(ch) && ch != '(' && ch != ')') {
result.push_back(ch); // 操作数直接输出
} else if (ch == '(') {
op_stack.push(ch);
} else if (ch == ')') {
// 把这对括号内部的运算符全部弹出,最后丢掉 '('
while (op_stack.top() != '(') {
result.push_back(op_stack.top());
op_stack.pop();
}
op_stack.pop(); // 丢掉 '('
} else {
while (!op_stack.empty() && op_stack.top() != '(' &&
priority_of(op_stack.top()) >= priority_of(ch)) {
result.push_back(op_stack.top());
op_stack.pop();
}
op_stack.push(ch);
}
}
// 扫描结束后,栈里剩下的运算符按顺序弹出
while (!op_stack.empty()) {
result.push_back(op_stack.top());
op_stack.pop();
}
return result;
}
// 新建一个表达式树节点(动态开点),返回编号
ll new_node(char ch, ll left_id, ll right_id) {
node_cnt++;
node[node_cnt].type = ch;
node[node_cnt].left = left_id;
node[node_cnt].right = right_id;
return node_cnt;
}
// 第二步:扫描后缀表达式建表达式树,返回根节点编号。
// 后缀里是「左操作数 右操作数 运算符」,所以弹栈时先弹出的是右儿子。
ll build_expr_tree() {
stack<ll> node_stack; // 建树时的节点栈,存放还没有被合并的子树根编号
for (ll i = 0; i < (ll)postfix.size(); i++) {
char ch = postfix[i];
if (!is_op(ch)) {
node_stack.push(new_node(ch, 0, 0)); // 操作数建成叶子
} else {
ll right_id = node_stack.top();
node_stack.pop();
ll left_id = node_stack.top();
node_stack.pop();
node_stack.push(new_node(ch, left_id, right_id));
}
}
return node_stack.top(); // 栈里最后剩下的就是整棵树的根
}
// 第三步:dfs 后根遍历表达式树,把求值和短路统计合在一起做。
// 后根遍历:先递归算完左子树,拿到它的值后才能判断要不要短路。
// 短路时整棵右子树都不递归,所以右子树内部的短路也就不会被统计。
void dfs_calc_value(ll u) {
if (!is_op(node[u].type)) {
node[u].value = node[u].type - '0'; // 叶子:值就是 '0'/'1' 本身
return;
}
dfs_calc_value(node[u].left); // 先递归算左子树
// a&b 中 a 为 0,或 a|b 中 a 为 1:短路,右子树不用求值
if (node[u].type == '&' && node[node[u].left].value == 0) {
short_and++;
node[u].value = 0;
return;
}
if (node[u].type == '|' && node[node[u].left].value == 1) {
short_or++;
node[u].value = 1;
return;
}
// 不短路:右子树也要递归求值,最后算自己
dfs_calc_value(node[u].right);
if (node[u].type == '&') {
node[u].value = node[node[u].left].value & node[node[u].right].value;
} else {
node[u].value = node[node[u].left].value | node[node[u].right].value;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
postfix = infix_to_postfix();
ll root = build_expr_tree();
dfs_calc_value(root);
cout << node[root].value << '\n';
cout << short_and << ' ' << short_or << '\n';
return 0;
}Python 短解法
Python 版可以更进一步:不显式建树,把“一段表达式的结果”记成三元组(值、& 短路次数、
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-05 15:21
# update_at: 2026-10-05 15:21
# 这个代码的特点: 不用真的建立表达式树,用调度场算法+短路计数来直接计算
# vals 里面直接存 计算的结果和短路的次数
import sys
type Value = tuple[int, int, int] # 一段表达式的结果:(值, & 短路次数, | 短路次数)
type Values = list[Value] # 调度场算法的操作数栈,栈里每段是一个 Value
PRIO = {'&': 2, '|': 1} # 运算符优先级:& 高于 |;'(' 不参与比较,取默认 0
def reduce_top(vals: Values, ops: list[str]) -> None:
"""归约一层:把栈顶运算符和左右两段结果合成一段,短路时丢弃右段的短路次数。"""
right = vals.pop()
left = vals.pop()
op = ops.pop()
lval, l_and, l_or = left
rval, r_and, r_or = right
if op == '&':
if lval == 0:
# a&b 中 a 为 0:整段值为 0,b 一整段都不求值,右段的短路次数全部作废
vals.append((0, l_and + 1, l_or))
else:
vals.append((lval & rval, l_and + r_and, l_or + r_or))
else:
if lval == 1:
# a|b 中 a 为 1:整段值为 1,b 一整段都不求值,右段的短路次数全部作废
vals.append((1, l_and, l_or + 1))
else:
vals.append((lval | rval, l_and + r_and, l_or + r_or))
def evaluate(s: str) -> Value:
"""调度场算法边扫描边归约,直接算出整个表达式的结果。"""
vals: Values = []
ops: list[str] = [] # 运算符栈,只放 '&', '|', '('
for ch in s:
if ch in '01':
vals.append((int(ch), 0, 0)) # 叶子:单个 0/1,自身没有短路
elif ch == '(':
ops.append(ch)
elif ch == ')':
while ops[-1] != '(':
reduce_top(vals, ops)
ops.pop() # 丢掉 '('
else:
# 栈顶“该先算”的运算符先归约;同级从左到右,所以 >= 就弹
while ops and ops[-1] != '(' and PRIO[ops[-1]] >= PRIO[ch]:
reduce_top(vals, ops)
ops.append(ch)
while ops:
reduce_top(vals, ops)
return vals[0] # 归约完栈里剩下的最后一段就是整个表达式
def solve() -> None:
# 输入就是一整行表达式本身,没有空白可切分,直接整行读
s = sys.stdin.readline().strip()
value, short_and, short_or = evaluate(s)
print(value)
print(short_and, short_or)
if __name__ == "__main__":
solve()时间复杂度同为
复杂度
建树和求值都只会对每个字符或节点做常数次处理,所以总时间复杂度是
总结
这题的关键不是普通表达式求值,而是“短路会让整棵右子树失去访问资格”。把表达式显式建成语法树后,这个限制就会变得很自然。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
