先按后缀表达式建树并求当前值,再从根向下传播“能否影响根”的标记,这样每个翻转询问都能 O(1) 回答。
OJ: luogu
题目 ID: P7073
难度:普及+/提高
标签:栈字符串思维
日期: 2026-06-19 21:43
题意
给出一个后缀表达式,以及每个变量的初始
每次询问翻转某一个变量的值,其余变量保持初始值不变,问整个表达式的新值。
思路
最直接的办法是每次询问都重新扫一遍整条后缀表达式。
先看一个可以直接验证想法的朴素解:
#include <bits/stdc++.h>
using namespace std;
vector<string> split_tokens(const string &s) {
vector<string> tokens;
int n = (int)s.size();
int i = 0;
while (i < n) {
while (i < n && s[i] == ' ') {
++i;
}
if (i >= n) {
break;
}
int j = i;
while (j < n && s[j] != ' ') {
++j;
}
tokens.push_back(s.substr(i, j - i));
i = j;
}
return tokens;
}
int evaluate_once(const vector<string> &tokens, const vector<int> &value, int flip_id) {
vector<int> st;
st.reserve(tokens.size());
for (const string &token : tokens) {
if (token[0] == 'x') {
int id = stoi(token.substr(1));
st.push_back(value[id] ^ (id == flip_id));
} else if (token == "!") {
int x = st.back();
st.pop_back();
st.push_back(x ^ 1);
} else {
int right = st.back();
st.pop_back();
int left = st.back();
st.pop_back();
if (token == "&") {
st.push_back(left & right);
} else {
st.push_back(left | right);
}
}
}
return st.back();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string expr;
getline(cin, expr);
vector<string> tokens = split_tokens(expr);
int n;
cin >> n;
vector<int> value(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> value[i];
}
int q;
cin >> q;
while (q--) {
int x;
cin >> x;
cout << evaluate_once(tokens, value, x) << '\n';
}
return 0;
}brute.cpp 对每个询问都重新模拟一次后缀表达式求值,逻辑简单,但复杂度是
关键观察:所有询问都基于同一组初始赋值,所以我们没必要每次都重算整棵表达式树。
建树
后缀表达式天然对应一棵表达式树,用栈就能建出来:
- 遇到变量
x_i:创建一个叶子节点,把节点编号入栈。 - 遇到一元运算符
!:从栈顶弹出一个节点作为孩子,创建非门节点,入栈。 - 遇到二元运算符
&或|:从栈顶弹出两个节点(先右后左),创建对应节点,入栈。
最后栈底剩下的就是根节点。
拿样例 x1 x2 & x3 | 举例,建树过程:
读 x1 → 栈: [1] (节点1 = x1)
读 x2 → 栈: [1,2] (节点2 = x2)
读 & → pop 2,1 → 栈: [3] (节点3 = &, 孩子 left=1, right=2)
读 x3 → 栈: [3,4] (节点4 = x3)
读 | → pop 4,3 → 栈: [5] (节点5 = |, 孩子 left=3, right=4)最终得到的 节点5 就是根 |,它的左孩子是 & 节点,右孩子是 x3。
第一趟 DFS:自底向上求值
dfs1(u) 递归求节点 u 的值:
- 叶子节点(变量):直接返回
init_val[var_id]。 - 非门:递归求孩子的值,取反。
- 与门:递归求左右孩子的值,
lval & rval。 - 或门:递归求左右孩子的值,
lval | rval。
这一趟结束后,每个节点都知道了自己的当前值,根节点的值就是整个表达式的值。
第二趟 DFS:自顶向下传播影响
核心问题是:翻转某个变量后,根的值会变吗?
答案取决于这个变量的改变能否沿着表达式树一路传到根。这就是 dfs2(u, about) 做的事。
about 表示“当前节点 u 的值发生变化时,是否会影响根节点的值”。
传播规则:
- 变量节点(叶子):直接记录
can_affect[u] = about——这就是最终答案。 - 非门
!u:把about原样传递给子节点。因为!只取反,孩子变了,非门的结果一定变。 - 与门
u & v:这里有关键的短路逻辑:- 如果
about == false(整个&节点不影响根),两个孩子更不可能影响根,直接传false。 - 如果
about == true,左孩子是否能影响根?答案是:只有当右孩子的当前值为1时。因为0 & x = 0已经被右路的0卡死,只有右路是1时1 & 1 = 1这个1才会随左路翻转。
- 如果
- 或门
u | v:逻辑对称:- 如果
about == false,两个孩子都传false。 - 如果
about == true,左孩子是否能影响根?答案是:只有当右孩子的当前值为0时。因为1 | x = 1已经被右路的1卡死,只有右路是0时0 | 0 = 0这个0才会随左路翻转。
- 如果
直观理解:
把 about 理解成一根从根放下来的绳子。每个门要么让绳子穿过去,要么剪断它。
!永远让绳子通过。&只有当另一边是1时才让绳子通过。如果另一边是0,绳子被卡断。|只有当另一边是0时才让绳子通过。如果另一边是1,绳子被卡断。
最后一根绳子连到某个变量叶子节点上,就说明翻转这个变量能改变根的结果。
回答询问
预处理出 can_affect[] 后,每个询问就是 O(1):
如果 can_affect[var_pos[x]] == true:
输出 root_val ^ 1
否则:
输出 root_val因为整个表达式树的所有输入都是确定的,翻转一个变量只会让路径上的节点值取反(如果它确实能影响根的话),所以根的值也一定取反,不存在其他情况。
代码
/**
* 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: 2025-11-28 15:41
* update_at: 2025-11-28 15:41
*/
/*
* 题目:[CSP-J 2020] 表达式 (luogu 7073)
* 核心思路:
* 1. 表达式为后缀表达式,通过栈将其转化为表达式树。
* 2. 第一次 DFS (dfs1):从下往上求出所有节点原本的值。
* 3. 第二次 DFS (dfs2):从上往下传递"当前节点的改变是否会影响最终结果"。
* - 对于非门 (!),影响直接向下传递。
* - 对于与门 (&),如果另一半的值为 1,则本侧的改变会影响整体结果;否则被短路。
* - 对于或门 (|),如果另一半的值为 0,则本侧的改变会影响整体结果;否则被短路。
* 4. 预处理结束后,针对每个查询只需 O(1) 查表。
*/
#include <iostream>
#include <string>
#include <vector>
#include <stack>
using namespace std;
// ===== 输入数据 =====
const int MAXN = 1e6+5;
int n, q;
int init_val[MAXN];
vector<string> tokens; // 后缀表达式的所有 token
// ===== 表达式树 =====
struct Node {
char type; // 'x' 变量, '!' 非门, '&' 与门, '|' 或门
int lch, rch;
int var_id;
int val;
} node[MAXN];
int node_cnt = 0;
int var_node[MAXN]; // 变量 x_i 对应的节点编号
stack<int> stk;
// ===== 预处理结果 =====
bool can_affect[MAXN]; // 该节点的变化是否影响根节点
// dfs1: 从下至上求出每个节点的值
int dfs1(int u) {
if (node[u].type == 'x') {
return node[u].val = init_val[node[u].var_id];
}
if (node[u].type == '!') {
int child_val = dfs1(node[u].lch);
return node[u].val = child_val ^ 1;
}
int lval = dfs1(node[u].lch);
int rval = dfs1(node[u].rch);
if (node[u].type == '&') {
return node[u].val = lval & rval;
}
return node[u].val = lval | rval;
}
// dfs2: 从根向下传播"影响标记"
// affects_root: 当前节点的变化是否会影响根节点
void dfs2(int u, bool affects_root) {
can_affect[u] = affects_root;
if (node[u].type == 'x') return;
if (!affects_root) {
dfs2(node[u].lch, false);
if (node[u].type != '!')
dfs2(node[u].rch, false);
return;
}
if (node[u].type == '!') {
dfs2(node[u].lch, true);
return;
}
int lch = node[u].lch, rch = node[u].rch;
if (node[u].type == '&') {
dfs2(lch, node[rch].val == 1);
dfs2(rch, node[lch].val == 1);
} else {
dfs2(lch, node[rch].val == 0);
dfs2(rch, node[lch].val == 0);
}
}
// 读入表达式所有 token 和变量个数 n
void init_tokens() {
string tok;
while (cin >> tok) {
if (tok[0] >= '0' && tok[0] <= '9') {
n = stoi(tok);
break; // 表达式 token 只含 x... ! & |,遇到数字就是 n
}
tokens.push_back(tok);
}
}
// 根据全局 tokens 建立表达式树,返回根节点编号
int build_tree() {
for (const string &tok : tokens) {
if (tok[0] == 'x') {
int id = stoi(tok.substr(1));
node[++node_cnt] = {'x', 0, 0, id, 0};
var_node[id] = node_cnt;
stk.push(node_cnt);
} else if (tok[0] == '!') {
int child = stk.top(); stk.pop();
node[++node_cnt] = {'!', child, 0, 0, 0};
stk.push(node_cnt);
} else {
int right = stk.top(); stk.pop();
int left = stk.top(); stk.pop();
node[++node_cnt] = {tok[0], left, right, 0, 0};
stk.push(node_cnt);
}
}
return stk.top();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init_tokens();
for (int i = 1; i <= n; i++) {
cin >> init_val[i];
}
int root = build_tree();
dfs1(root);
dfs2(root, true);
cin >> q;
int root_val = node[root].val;
while (q--) {
int x;
cin >> x;
if (can_affect[var_node[x]])
cout << (root_val ^ 1) << '\n';
else
cout << root_val << '\n';
}
return 0;
}复杂度
建树、求值、影响传播都是线性的,所以预处理复杂度是
总结
这题的难点不在后缀表达式本身,而在看出"翻转一个变量是否有用"可以预处理。把"每次重算整式"改成"先判断哪些变量能影响根",复杂度就从
另一个重要的思维是短路传播:二叉树中一个节点的值是否受子树影响,往往取决于另一个子树的值。这种"一侧卡死另一侧"的模式在表达式求值、决策树剪枝中都可能出现。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
