后缀表达式建树后正序求值、逆序传播翻转信号:非运算恒穿过,与运算看兄弟是否为 1,或运算看兄弟是否为 0,每次询问 O(1) 作答,总复杂度 O(|s|+q)。
OJ: roj
题目 ID: 3666
难度:普及+/提高-
标签:栈树递推python
创建: 2026-10-02 14:23
更新: 2026-10-10 11:30
形式化题目
给定一个后缀(逆波兰)表达式,操作数是变量
a b &:与,仅当两者都为时结果为 ; a b |:或,仅当两者都为时结果为 ; a !:取反,变 、 变 。
有
样例 1 的后缀式 x1 x2 & x3 | 对应中缀 1 1 0。样例 2 的 x1 ! x2 x4 | x3 x5 ! & & ! & 对应 0 1 1。
正解
思路
朴素做法与瓶颈。 每次询问把变量取反后重新求值:单次 & 左结合)深度
关键观察:变量只出现一次,翻转的影响只有一条路径,且可以静态判定。 翻转
!:孩子翻则自己翻,恒穿过;&:输出锁死为的情形是兄弟为 ,故兄弟为 才穿过; |:输出锁死为的情形是兄弟为 ,故兄弟为 才穿过。
定义翻转信号
递推合法的原因:翻转 child 若过不了这道门,parent 的值不变、根自然不变;若过了,parent 的值取反而其余输入与原情形完全相同,根的结果就等于"原情形下单独翻转 parent",即
预处理三步 + 询问查表:
- 建树:后缀表达式用栈一遍扫描建出表达式树;按 token 顺序编号,孩子一定先于父结点产生;
- 正序求值:孩子编号小于父编号,正序扫一遍就是拓扑序,得到每个结点的原值(穿过条件的依据);
- 逆序下传信号:根编号最大、父大于子,逆序扫一遍(反拓扑序)把
按递推式下传到所有叶子。
此后每次询问
下图在样例 1 的表达式树上标出每个结点的原值与信号
flowchart TD
R["|(根)<br/>值 1,信号 1"] --> A["&<br/>值 0,信号 0"]
R --> X3["x3<br/>值 1,信号 1"]
A --> X1["x1<br/>值 1,信号 0"]
A --> X2["x2<br/>值 0,信号 0"]
根是或门:判断左孩子 & 的信号要看兄弟 & 子树信号全为 & 的值
| 询问 | 翻转的变量 | 该叶子的信号 |
答案 |
|---|---|---|---|
| 1 | 0 | 1 | |
| 2 | 0 | 1 | |
| 3 | 1 | 0 |
翻 1 1 0 完全一致。
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int AND=0, OR=1, NOT=2;
struct Child{int l,r;};
int calc(int op,int l,int r){ if(op==AND) return l&r; if(op==OR) return l|r; return l^1; }
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); vector<string> tokens; string tok; while(cin>>tok) tokens.push_back(tok); if(tokens.empty()) return 0; int pos=0; vector<string> expr; while(pos<(int)tokens.size() && !(tokens[pos][0]>='0'&&tokens[pos][0]<='9')) expr.push_back(tokens[pos++]); int n=atoi(tokens[pos++].c_str()); vector<int> val(n); for(int i=0;i<n;i++) val[i]=atoi(tokens[pos++].c_str()); int q=atoi(tokens[pos++].c_str()); vector<int> op_of, leaf_val, var_leaf(n,-1), stack; vector<Child> child; for(int ti=0;ti<(int)expr.size();ti++){string s=expr[ti]; int v=op_of.size(); if(s=="!"){int c=stack.back(); stack.pop_back(); stack.push_back(v); op_of.push_back(NOT); child.push_back({c,-1}); leaf_val.push_back(-1);} else if(s=="&"||s=="|"){int cr=stack.back(); stack.pop_back(); int cl=stack.back(); stack.pop_back(); stack.push_back(v); op_of.push_back(s=="&"?AND:OR); child.push_back({cl,cr}); leaf_val.push_back(-1);} else {int id=atoi(s.c_str()+1)-1; stack.push_back(v); op_of.push_back(-1); child.push_back({-1,-1}); leaf_val.push_back(val[id]); var_leaf[id]=v;}} int root=stack[0]; vector<int> node_val(op_of.size()); for(int v=0;v<(int)op_of.size();v++){int op=op_of[v]; if(op<0) node_val[v]=leaf_val[v]; else {int cl=child[v].l, cr=child[v].r; node_val[v]=calc(op,node_val[cl],cr>=0?node_val[cr]:0);}} vector<int> sens(op_of.size()); sens[root]=1; for(int v=(int)op_of.size()-1;v>=0;v--){int op=op_of[v]; if(op<0) continue; int cl=child[v].l, cr=child[v].r; if(op==NOT){sens[cl]=sens[v]; continue;} int pass_l=op==AND?node_val[cr]:1-node_val[cr]; int pass_r=op==AND?node_val[cl]:1-node_val[cl]; sens[cl]=sens[v]&pass_l; sens[cr]=sens[v]&pass_r;} for(int i=0;i<q;i++){int id=atoi(tokens[pos++].c_str()); cout<<(node_val[root]^sens[var_leaf[id-1]])<<'\n';} return 0;}#!/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-02 14:31
# update_at: 2026-10-02 14:54
import sys
AND = 0 # 结点运算符编码:0 = &,1 = |,2 = !,-1 = 变量叶子
OR = 1
NOT = 2
def calc(op: int, l: int, r: int) -> int:
"""按运算符合成左右孩子的值;! 只用左值,右位的 -1 是占位。"""
if op == AND:
return l & r
if op == OR:
return l | r
return l ^ 1
def build(tokens: list[bytes], val: list[int]) -> tuple[list[int], list[tuple[int, int]], list[int], list[int], int]:
"""后缀表达式逐词建树:变量出叶子,运算符不断把栈顶片段合并成更大片段。
每个变量在表达式中恰好出现一次,叶子按变量下标登记,与出现顺序无关。
返回 (op_of, child, leaf_val, var_leaf, root):op_of[v] 是 v 的运算符(叶子为 -1),
child[v] 是 (左孩子, 右孩子)(! 没有右孩子,右位记 -1),leaf_val[v] 是叶子初值
(内部结点记 -1),var_leaf[i] 是变量 xi 的叶子。结点按 token 顺序编号,
孩子一定先于父结点出现,所以正序是拓扑序、逆序是反拓扑序。
"""
var_leaf = [-1] * len(val) # 变量下标 → 叶子结点编号
op_of: list[int] = []
child: list[tuple[int, int]] = []
leaf_val: list[int] = []
stack: list[int] = [] # 栈里是各"表达式片段"的根结点编号
for tok in tokens:
v = len(op_of) # 新结点编号
if tok == b'!':
c = stack.pop() # 一元运算:唯一的孩子放左位
stack.append(v)
op_of.append(NOT)
child.append((c, -1))
leaf_val.append(-1)
elif tok == b'&' or tok == b'|':
cr = stack.pop() # 右操作数先进栈,先弹的是右孩子
cl = stack.pop()
stack.append(v)
op_of.append(AND if tok == b'&' else OR)
child.append((cl, cr))
leaf_val.append(-1)
else: # token 形如 x10:x + 正整数下标
i = int(tok[1:]) - 1
stack.append(v)
op_of.append(-1)
child.append((-1, -1))
leaf_val.append(val[i])
var_leaf[i] = v
return op_of, child, leaf_val, var_leaf, stack[0]
def fill_values(op_of: list[int], child: list[tuple[int, int]], leaf_val: list[int]) -> list[int]:
"""自底向上求每个结点的值:孩子编号一定小于父结点,正序扫一遍就是拓扑序。"""
node_val = [0] * len(op_of)
for v, op in enumerate(op_of):
if op < 0: # 变量叶子:值就是输入给的初值
node_val[v] = leaf_val[v]
else:
cl, cr = child[v]
node_val[v] = calc(op, node_val[cl], node_val[cr] if cr >= 0 else 0)
return node_val
def fill_sens(root: int, op_of: list[int], child: list[tuple[int, int]], node_val: list[int]) -> list[int]:
"""反向传播"翻转信号":sens[v] = 1 表示把 v 的值取反能让根的值也取反。
父结点编号一定大于孩子,逆序扫(根最先)就能保证下传时父结点的信号已就位。
信号穿过结点的条件只看兄弟的原值——变量各出现一次,翻转不会影响兄弟。
"""
sens = [0] * len(op_of)
sens[root] = 1 # 根自己取反,答案当然跟着取反
for v in range(len(op_of) - 1, -1, -1):
op = op_of[v]
if op < 0:
continue # 叶子是信号终点,答案查询时直接读它
cl, cr = child[v]
if op == NOT:
sens[cl] = sens[v] # 取反不吸收信号,原样传给唯一的孩子
continue
# 与门要兄弟为 1、或门要兄弟为 0,孩子的翻转才穿得过本结点
pass_l = node_val[cr] if op == AND else 1 - node_val[cr]
pass_r = node_val[cl] if op == AND else 1 - node_val[cl]
sens[cl] = sens[v] & pass_l
sens[cr] = sens[v] & pass_r
return sens
def solve() -> None:
tokens = sys.stdin.buffer.read().split()
it = iter(tokens)
# 表达式 token 只有 x 开头的变量与 & | !,读到第一个纯数字(就是 n)为止
expr_tokens: list[bytes] = []
for tok in it:
if tok.isdigit():
data = iter(map(int, [tok] + list(it)))
break
expr_tokens.append(tok)
n = next(data)
val = [next(data) for _ in range(n)] # val[i]:xi 的初值
q = next(data)
op_of, child, leaf_val, var_leaf, root = build(expr_tokens, val)
node_val = fill_values(op_of, child, leaf_val)
sens = fill_sens(root, op_of, child, node_val)
# 每个询问只临时翻一个变量:答案 = 根值 XOR 该叶子的翻转信号,O(1) 作答
out: list[str] = []
for _ in range(q):
idx = next(data)
out.append(str(node_val[root] ^ sens[var_leaf[idx - 1]]))
print('\n'.join(out))
if __name__ == "__main__":
solve()build() 对应第 1 步:op_of/child/leaf_val 是结点三元组,var_leaf 按 token 内的变量下标登记叶子(出现顺序与下标无关),stack[0] 即根;fill_values() 对应第 2 步,正序枚举、叶子取初值、内部结点交给 calc() 合成;fill_sens() 对应第 3 步,sens[root] = 1 起步逆序下传,其中 pass_l/pass_r 就是穿过条件(与门取兄弟原值、或门取兄弟的非),NOT 分支不读右孩子。注意下标约定:正文变量是 1 基的 var_leaf[i - 1] 与询问行的 int(...) - 1 都先减 1;! 结点没有右孩子,child 右位存 -1,求值时以 0 占位(calc 对 NOT 只用左值)。
复杂度
- 时间:建树、求值、信号下传三遍扫描各
,询问每条 ,合计 ,与正解同阶;朴素的 与爬树的 都被消除。 - 空间:token 列表与结点数组
。 - 实测 20 组真实数据全部输出一致,最慢测例 0.124s、峰值约 84MB(OJ 限制 1000ms / 128MB,本地为 Python 放宽计时口径;Python 若在 OJ 上 TLE/MLE 按仓库约定可接受,算法本身与 C++ 正解同阶)。
总结
- 变量唯一出现是全题支点:翻转只沿一条路径传播、兄弟子树值不变,"翻转能否穿过这道门"因此可用原值静态判定——非恒穿过、与看兄弟为
、或看兄弟为 。 - 把"翻转是否传到根"抽象成信号
自顶向下递推,三遍 扫描换每次询问 :建树 → 正序求值 → 逆序下传。 - 后缀表达式按 token 编号天然给出拓扑序:正序求值、逆序下传,无需递归或父指针,也避开了链状深树的深度瓶颈。
- 易错点:叶子必须按变量下标登记(出现顺序≠下标顺序);
!是一元运算,右孩子要占位且两种扫描都不能碰它。