先按优先级把中缀表达式建成语法树,再用显式栈按短路语义迭代求值,只统计真正访问到的子树里的短路次数。
OJ: luogu
题目 ID: P8815
难度:普及+/提高
标签:栈字符串模拟
日期: 2026-06-19 20:55
题意
给出一个只包含 0、1、&、
需要输出:
- 表达式最终的值
- 形如
a&b的短路次数 - 形如
的短路次数
题目规定括号优先,& 高于
思路
最直接的办法是按文法递归下降解析,并在解析过程中按短路语义求值。
先看一个可以直接验证想法的朴素解:
cpp
#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,右边整块都不会被访问。
因此右子树内部的任何短路,也都不该被统计。
所以正式解分成两步:
- 先用运算符栈按优先级把中缀表达式建成语法树
- 再用显式栈模拟递归求值过程
求值时:
- 先处理左子树
- 若当前是
&且左值为0,统计一次&短路并直接返回 - 若当前是
且左值为 1,统计一次短路并直接返回 - 否则继续处理右子树,再合并最终值
这样既满足短路定义,也避免了深递归。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXL = 1000000 + 5;
static int left_son[MAXL];
static int right_son[MAXL];
static char node_type[MAXL];
static unsigned char node_value[MAXL];
static int node_cnt;
static int value_stack[MAXL];
static char op_stack[MAXL];
static int eval_node_stack[MAXL];
static unsigned char eval_state_stack[MAXL];
int priority_of(char ch) {
if (ch == '|') {
return 1;
}
if (ch == '&') {
return 2;
}
return 0;
}
int new_node(char ch, int left_id, int right_id) {
++node_cnt;
node_type[node_cnt] = ch;
left_son[node_cnt] = left_id;
right_son[node_cnt] = right_id;
return node_cnt;
}
void apply_once(int &value_top, int &op_top) {
char op = op_stack[op_top--];
int right_id = value_stack[value_top--];
int left_id = value_stack[value_top--];
value_stack[++value_top] = new_node(op, left_id, right_id);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
cin >> s;
int value_top = 0;
int op_top = 0;
for (char ch : s) {
if (ch == '0' || ch == '1') {
value_stack[++value_top] = new_node(ch, 0, 0);
} else if (ch == '(') {
op_stack[++op_top] = ch;
} else if (ch == ')') {
while (op_top > 0 && op_stack[op_top] != '(') {
apply_once(value_top, op_top);
}
--op_top;
} else {
while (op_top > 0 && op_stack[op_top] != '(' &&
priority_of(op_stack[op_top]) >= priority_of(ch)) {
apply_once(value_top, op_top);
}
op_stack[++op_top] = ch;
}
}
while (op_top > 0) {
apply_once(value_top, op_top);
}
int root = value_stack[value_top];
long long short_and = 0;
long long short_or = 0;
int eval_top = 1;
eval_node_stack[1] = root;
eval_state_stack[1] = 0;
// 迭代模拟递归求值,只在真正访问到的子树里统计短路次数。
while (eval_top > 0) {
int u = eval_node_stack[eval_top];
char ch = node_type[u];
if (ch == '0' || ch == '1') {
node_value[u] = (ch == '1');
--eval_top;
continue;
}
if (eval_state_stack[eval_top] == 0) {
eval_state_stack[eval_top] = 1;
eval_node_stack[++eval_top] = left_son[u];
eval_state_stack[eval_top] = 0;
continue;
}
if (eval_state_stack[eval_top] == 1) {
if (ch == '&' && node_value[left_son[u]] == 0) {
++short_and;
node_value[u] = 0;
--eval_top;
continue;
}
if (ch == '|' && node_value[left_son[u]] == 1) {
++short_or;
node_value[u] = 1;
--eval_top;
continue;
}
eval_state_stack[eval_top] = 2;
eval_node_stack[++eval_top] = right_son[u];
eval_state_stack[eval_top] = 0;
continue;
}
if (ch == '&') {
node_value[u] = node_value[left_son[u]] & node_value[right_son[u]];
} else {
node_value[u] = node_value[left_son[u]] | node_value[right_son[u]];
}
--eval_top;
}
cout << (int)node_value[root] << '\n';
cout << short_and << ' ' << short_or << '\n';
return 0;
}复杂度
建树和求值都只会对每个字符或节点做常数次处理,所以总时间复杂度是
总结
这题的关键不是普通表达式求值,而是“短路会让整棵右子树失去访问资格”。把表达式显式建成语法树后,这个限制就会变得很自然。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
