You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
7.9 KiB

2 years ago
## $[CSP-J2020]$ 表达式
**[[$CSP-J2020$]表达式-洛谷](https://www.luogu.com.cn/problem/P7073)**
思路:**后缀表达式** 用 **栈** 模拟运算
不做任何处理,**暴力**可以得$30$分
### 一、题目解析
先看样例:$x_1\ x_2 \ \& \ x_3 \ |$ 这是一个后缀表达式形式,
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/03/ba55dcc4281c548e8cebee0a582f89de.png)
**可以用测试用例给同学们讲解一下测试用例,包括第一次计算和修改某个值后的计算。**
### 一、暴力建树求值
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010, M = N << 1;
#define ls e[h[u]]
#define rs e[ne[h[u]]]
int n; // n个变量
int a[N]; // 每个变量对应的数值
char c[N]; // 操作符
stack<int> stk; // 模拟栈
// 链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
// 表示式求值
int dfs(int u) {
if (u <= n) return a[u]; // 如果是叶子,是变量,是数值,返回真实值
if (c[u] == '!')
a[u] = !dfs(ls); //! 只有一个儿子所以e[h[u]]就是儿子的节点号,对儿子返回的值取反就是当前节点的返回值,同时实现了记忆化
else {
//&和|有两个儿子分别是e[h[u]]和e[ne[h[u]]]
if (c[u] == '&')
a[u] = dfs(ls) & dfs(rs); // 计算
else
a[u] = dfs(ls) | dfs(rs); // 计算
}
return a[u]; // 返回
}
// https://www.luogu.com.cn/problem/P7073
// 20个测试点可以过掉6个
int main() {
#ifndef ONLINE_JUDGE
freopen("P7073.in", "r", stdin);
// freopen(".out", "w", stdout);
#endif
memset(h, -1, sizeof h); // 初始化链式前向星
string s;
getline(cin, s);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i]; // x1,x2,x3的真实值
// 数字占了前n个节点,比如x1占用了1号节点x2占用了2号节点
// n+1开始留给操作符
int idx = n;
for (int i = 0; i < s.size(); i++) {
if (s[i] == ' ') continue; // 放过空格
if (s[i] == 'x') { // 发现是变量标识
int k = 0;
i++;
while (i < s.size() && isdigit(s[i])) k = k * 10 + s[i++] - '0'; // x?,?=124
stk.push(k); // x124存到图中节点号就是124
} else if (s[i] == '!') {
c[++idx] = s[i]; // 记录操作符数组,n+1是第一个操作符对应的树中节点号
add(idx, stk.top()); // 操作符向数字连一条边
stk.pop();
stk.push(idx); // 将操作符也要入栈
} else { // 如果是 & 或 |
c[++idx] = s[i]; // 记录idx号节点是 & 或 | 或 !
int a = stk.top();
stk.pop();
int b = stk.top();
stk.pop();
add(idx, a), add(idx, b); // 1托2建边
stk.push(idx); // 将操作符也要入栈
}
}
// 最后一个入栈的是根,根是一个操作符,叶子都是变量,是数值
int root = stk.top();
int q;
cin >> q;
while (q--) {
int x;
cin >> x;
// 取反
a[x] = !a[x];
// 输出
cout << dfs(root) << endl;
// 回溯
a[x] = !a[x];
}
return 0;
}
```
### 二、优化
介绍一下$\& | ! $
> $\&$ $1\&1$为$1$,其余为$0$
> $1 \& 0 = 0$
> $1 \& 1 = 1$
> $0 \& 0 = 0$
> $0 \& 1 = 0$
> $|$ : $0|0$为$0$,其余为$1$
> $1 | 0 = 1$
> $1 | 1 = 1$
> $0 | 0 = 0$
> $0 | 1 = 1$
> $$
> $! 0 = 1$
> $! 1 = 0$
发现了吗?对于$\&$和$|$都有一些运算数 **无论怎样改变**,只要 **另一个运算数不变**,它们 **运算的值也不变**。
有以下结论:
> 当$x\&y$时:
> $x$为$0$时,$y$无法造成任何影响
> $y$为$0$时,$x$无法造成任何影响
> 当$x|y$时:
> $x$为$1$时,$y$无法造成任何影响
> $y$为$1$时,$x$无法造成任何影响
> 当$!x$时:
> $x$一定改变
那么我们可以把 **可以造成影响的数** 打上一个 **$st$标记**
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230508092420.png)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 重定义左右儿子
#define ls e[h[u]]
#define rs e[ne[h[u]]]
const int N = 1000010, M = N << 1;
int n;
int a[N]; // 每个变量的数值0或1
char c[N]; // 记录 c[i]是哪种操作符,比如 & | !,如果是变量x1,x2,x3 ... 节点,则默认值是0
stack<int> stk; // 建立表示树用到的栈
// 邻接表
int e[M], h[N], idx, ne[M];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 异或表示式树求值计算
int dfs1(int u) {
if (u <= n) return a[u];
if (c[u] == '!')
a[u] = !dfs1(ls);
else {
if (c[u] == '&')
a[u] = dfs1(ls) & dfs1(rs);
else if (c[u] == '|')
a[u] = dfs1(ls) | dfs1(rs);
}
return a[u];
}
// 标记以u为根的子树每个数值结点叶子结点变更是否对整体结果有影响,true:有影响false:无影响
int st[N];
void dfs2(int u) {
if (u <= n) { // 叶子节点
st[u] = 1; // 这个叶子节点的修改,对整体结果有影响。如果无法到达这个位置,就表示无影响
return;
}
if (c[u] == '!') // 非运算符,左儿子的变化会影响结果
dfs2(ls);
else {
if (c[u] == '&') {
// ① 如果左儿子是1右儿子会影响结果
// ② 如果右儿子是1左儿子会影响结果
if (a[ls]) dfs2(rs);
if (a[rs]) dfs2(ls);
} else if (c[u] == '|') {
// ③ 如果左儿子是0右儿子会影响结果
// ④ 如果右儿子是0左儿子会影响结果
if (!a[ls]) dfs2(rs);
if (!a[rs]) dfs2(ls);
}
}
}
int main() {
string s;
getline(cin, s);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
// 初始化
memset(h, -1, sizeof h);
// 放过前n个从n+1开始
int idx = n;
for (int i = 0; i < s.size(); i++) {
if (s[i] == ' ') continue;
if (s[i] == 'x') {
int k = 0;
i++;
while (i < s.size() && isdigit(s[i])) k = k * 10 + s[i++] - '0';
stk.push(k);
} else if (s[i] == '!') {
c[++idx] = s[i];
add(idx, stk.top());
stk.pop();
stk.push(idx);
} else {
c[++idx] = s[i];
int x = stk.top();
stk.pop();
int y = stk.top();
stk.pop();
add(idx, x), add(idx, y);
stk.push(idx);
}
}
int root = stk.top();
// 计算初始值,因为后面的修改,可能不改变原始值,也可能改变原始值
// 首先我们要计算出每个操作符所在位置的原始结果值
int res = dfs1(root);
// 标记每个数值结点(叶子结点)变更,是否对整体结果有影响,true:有影响false:无影响
dfs2(root);
// 处理q次询问
int q;
cin >> q;
// 询问时查表即可,整个程序的时间复杂度为O(q)
while (q--) {
int x;
cin >> x;
if (st[x]) // 如果x被打过标记那么它的变化将会影响根节点的值对根节点异或1即可
printf("%d\n", res ^ 1);
else // 不会影响根节点的值
printf("%d\n", res);
}
return 0;
}
```