前言
本题主要使用了数据结构表达式树。其实只要掌握了思路本题并不难,难的是考虑到怎么每次不去遍历更改整棵树而是通过一些预处理来使查询变快。
题目大意
在这种逻辑表达式中,所有操作数都是变量,且它们的取值只能为 0 0 0 或 1 1 1,运算从左往右进行。如果表达式中有括号,则先计算括号内的子表达式的值。特别的,这种表达式有且仅有以下几种运算:
- 与运算:
a & b
。 - 或运算:
a | b
。 - 取反运算:
!a
。
小 C 想知道,给定一个逻辑表达式和其中每一个操作数的初始取值后,再取反某一个操作数的值时,原表达式的值为多少。
表达式将采用后缀表达式的方式输入。
- 对于 100 % 100\% 100% 的数据, 1 ≤ ∣ s ∣ ≤ 1 × 1 0 6 1 \le |s| \le 1 \times 10^6 1≤∣s∣≤1×106, 1 ≤ q ≤ 1 × 1 0 5 1 \le q \le 1 \times 10^5 1≤q≤1×105, 2 ≤ n ≤ 1 × 1 0 5 2 \le n \le 1 \times 10^5 2≤n≤1×105。
思路
表达式树
什么是表达式树?
表达式树,是将一个表达式的值转换成一颗二叉树。
表达式树有这些性质:
- 在表达式树上,叶子结点均为数字,其余的普通结点为运算符。
- 一个表达式的中缀表达式对应着表达式树的中序遍历顺序,后缀表达式对应着表达式树的后序遍历顺序。前缀相同。
使用后缀表达式十分方便的就可以建立表达式树。
结点记录的信息
每一个结点至少要记录当前结点的值,操作符类型,左右孩子的指针(或伪指针,用数组)。其余的因题而异。
同时如果使用数组,那么空间复杂度开到 O ( 表达式长度 ) ) \mathcal{O}(\text{表达式长度})) O(表达式长度)) 最保险。
struct node{
ll op,value=0,l=0,r=0;//分别为操作符类型,值,左右孩子的指针。
}t[MAXN];//MAXN为表达式长度
构建顺序
在后缀表达式中,由于叶子结点是数字,那么我们在碰到了数字时就添加一颗叶子结点,同时在一个栈(叫它sta
了)中添加当前结点的指针。之后如果是运算符,那么从sta
中取出前两个(如果合法的话就没问题)元素,之后做这个运算符的计算,再把当前的指针放进式子里。
不妨可以看出,最后一个运算符的指针就是这棵树的根。
本题思路
在对表达式建立了一个表达式树以后,可以轻松地想出一个暴力做法,对于每一次询问,遍历这棵树,然后更改,之后再往上回溯,再去判断。看了看 n n n,再看了看 q q q,算了。
之后可以发现,其实题目要问的就是更改了这个数字对树根是否造成影响。如果能理解到这个题目也差不多可以想出来了。很多时候无论更不更改都是没有意义的。如x|1
,0&x
。
所以,我们可以再记录一个东西,为更改了这个结点是否会对父节点造成影响。由于更改了根结点一定会对根结点造成影响(这不废话),所以可以先把根结点的值设为 1 1 1( 1 1 1 是有影响, 0 0 0 相反)。之后看孩子,如果更改后的结果跟当前的原结果不一样,那么就让孩子的值等同于当前结点的值,这里的值就是另开一个变量存。
最后只需要判断给的询问的结点变量是否为 1 1 1 即可,是的话返回取反的,不是输出原结果。这样我们就做到了 O ( n ) \mathcal{O}(n) O(n) 预处理, O ( 1 ) \mathcal{O}(1) O(1) 查询。
代码
#include <iostream>
#include <stack>
using namespace std;
typedef long long ll;
const ll MAXN=1e6+5;
struct node{
ll op,value=0,l=0,r=0;
bool change= false;
}t[MAXN];
string s;
stack<ll>sta;
int main(){
getline(cin,s);
ll n;
scanf("%lld",&n);
for (int i = 1; i <=n ; ++i) {
scanf("%lld",&t[i].value);
t[i].op=0;
}
ll root=n;
for (int i = 0; i <s.size() ; ++i) {
if(s[i]=='x'){
ll num=0;
++i;
while (s[i]>='0'&&s[i]<='9'){
num*=10;
num+=(s[i]-'0');
++i;
}
sta.push(num);
}else if(s[i]=='&'){
t[++root].op=1;
t[root].l=sta.top();
sta.pop();
t[root].r=sta.top();
sta.pop();
t[root].value=t[t[root].l].value&t[t[root].r].value;
sta.push(root);
}else if(s[i]=='|'){
t[++root].op=2;
t[root].l=sta.top();
sta.pop();
t[root].r=sta.top();
sta.pop();
t[root].value=t[t[root].l].value|t[t[root].r].value;
sta.push(root);
}else if(s[i]=='!'){
t[++root].op=3;
t[root].l=sta.top();
sta.pop();
t[root].value=!t[t[root].l].value;
sta.push(root);
}
}
t[root].change= true;
for (ll i = root; i >=n+1 ; --i) {
ll l=t[i].l,r=t[i].r;
if(t[i].op==1){
if(((!t[l].value)&t[r].value)!=t[i].value){
t[l].change=t[i].change;
}
if((t[l].value&(!t[r].value))!=t[i].value){
t[r].change=t[i].change;
}
}else if(t[i].op==2){
if(((!t[l].value)|t[r].value)!=t[i].value){
t[l].change=t[i].change;
}
if((t[l].value|(!t[r].value))!=t[i].value){
t[r].change=t[i].change;
}
}else if(t[i].op==3){
t[l].change=t[i].change;
}
}
ll q;
scanf("%lld",&q);
for (int T = 1; T <=q ; ++T) {
ll ask;
scanf("%lld",&ask);
if(!t[ask].change){
cout<<t[root].value<<endl;
}else{
cout<<!t[root].value<<endl;
}
}
return 0;
}
总结
学习了表达式树的概念和构建方法。本题我们通过建了表达式树后又做了一次预处理使时间复杂度大幅降低。