Files
python/TangDou/CSP-J/J2_2020/ZhuanTi_BiaoDaShi/表达式求值专题.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

776 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 表达式求值专题
### 一、理论知识
#### [$22$张图带你深入剖析前缀、中缀、后缀表达式以及表达式求值](https://www.cnblogs.com/littlehb/p/16706908.html)
####[前缀/中缀/后缀----表达式之间的相互转换](https://blog.csdn.net/qq_40877281/article/details/115824864)
![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2023/04/9cc1e6409de974500ebc4934e320190b.png)
### 二、练习题
#### 1、中缀转后缀
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 中缀表达式转后缀表达式
/*
测试用例1:
a+b*c+(d*e+f)*g
答案:
abc*+de*f+g*+
测试用例2:
(6+3*(7-4))-8/2
答案:
6 3 7 4 - * + 8 2 / -
测试用例3:
(24*(9+6/3-5)+4)
答案:
24 9 6 3 / + 5 - * 4 +
测试用例4:
2*3*2+5/3
*/
unordered_map<char, int> g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 操作符优化级
string t; // 结果串
stack<char> stk; // 栈
// 中缀转后缀
string infixToPostfix(string s) {
for (int i = 0; i < s.size(); i++) {
// ①数字
if (isdigit(s[i])) {
int x = 0;
while (i < s.size() && isdigit(s[i])) {
x = x * 10 + s[i] - '0';
i++;
}
i--;
t += to_string(x), t += ' ';
} else if (isalpha(s[i])) // ②字符,比如a,b,c
t += s[i], t += ' ';
else if (s[i] == '(') // ③左括号
stk.push(s[i]); // 左括号入栈
else if (s[i] == ')') { // ④右括号
while (stk.top() != '(') { // 让栈中元素(也就是+-*/和左括号)一直出栈,直到匹配的左括号出栈
t += stk.top(), t += ' ';
stk.pop();
}
stk.pop(); // 左括号也需要出栈
} else {
// ⑤操作符 +-*/
while (stk.size() && g[s[i]] <= g[stk.top()]) {
t += stk.top(), t += ' ';
stk.pop();
}
stk.push(s[i]); // 将自己入栈
}
}
// 当栈不为空时,全部输出
while (stk.size()) {
t += stk.top(), t += ' ';
stk.pop();
}
return t;
}
int main() {
string infix = "(24*(9+6/3-5)+4)";
string postfix = infixToPostfix(infix);
cout << "中缀表达式: " << infix << endl;
cout << "后缀表达式: " << postfix << endl;
return 0;
}
```
#### 2、中缀转后缀(逻辑)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
/*
中缀逻辑表达式转后缀逻辑表达式
测试用例:
0&(0|1|0)
答案:
001|0|&
*/
unordered_map<char, int> g{{'|', 1}, {'&', 2}};
string s, t;
stack<char> stk;
int main() {
cin >> s;
for (int i = 0; i < s.size(); i++) {
if (isdigit(s[i]) || isalpha(s[i]))
t += s[i];
else if (s[i] == '(')
stk.push(s[i]);
else if (s[i] == ')') {
while (stk.top() != '(') {
t += stk.top();
stk.pop();
}
stk.pop();
} else {
while (stk.size() && g[s[i]] <= g[stk.top()]) {
t += stk.top();
stk.pop();
}
stk.push(s[i]);
}
}
while (stk.size()) {
t += stk.top();
stk.pop();
}
cout << t << endl;
return 0;
}
```
#### 3、后缀表达式求值
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 计算后缀表达式的值
// 判断字符是否是操作符
bool isOp(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
// 后缀表达式求值
int eval(string s) {
stack<int> stk; // 后缀表达式求值要使用到一个栈
// 构造一个字符串流,方便处理带空格的后缀表达式
stringstream s1(s);
string c;
while (s1 >> c) {
if (isOp(c[0])) { // 如果是操作数,将其转换为整数并入栈
int a = stk.top();
stk.pop();
int b = stk.top();
stk.pop();
if (c == "+") stk.push(b + a);
if (c == "-") stk.push(b - a);
if (c == "*") stk.push(b * a);
if (c == "/") stk.push(b / a);
} else
stk.push(stoi(c));
}
return stk.top();
}
int main() {
/*
中缀表达式: (24*(9+6/3-5)+4)
后缀表达式: 24 9 6 3 / + 5 - * 4 +
值=148
*/
string s = "24 9 6 3 / + 5 - * 4 +";
cout << "计算结果为: " << eval(s) << endl;
return 0;
}
```
#### [4、$AcWing$ $3302$. 中缀表达式求值](https://www.acwing.com/problem/content/3305/)
> 加减乘除四则运算,**利用栈**,**中缀表达式求值**,运算符优先顺序表
**直接计算版本**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
/*
测试用例I:
(2+2)*(1+1)
答案:8
测试用例II:
2+(3*4)-((5*9-5)/8-4)
答案:13
*/
stack<int> num; // 数字栈
stack<char> op; // 操作符栈
// 优先级表
unordered_map<char, int> g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}};
/**
* 功能:计算两个数的和差积商
*/
void eval() {
int a = num.top(); // 第二个操作数
num.pop();
int b = num.top(); // 第一个操作数
num.pop();
char p = op.top(); // 运算符
op.pop();
int r; // 结果
// 计算结果
if (p == '+') r = b + a;
if (p == '-') r = b - a;
if (p == '*') r = b * a;
if (p == '/') r = b / a;
// 结果入栈
num.push(r);
}
int main() {
// 读入表达式
string s;
cin >> s;
// 遍历字符串的每一位
for (int i = 0; i < s.size(); i++) {
// ① 如果是数字,则入栈
if (s[i] >= '0' && s[i] <= '9') {
// 读出完整的数字
int x = 0;
while (i < s.size() && s[i] >= '0' && s[i] <= '9') {
x = x * 10 + s[i] - '0';
i++;
}
i--; // 加多了一位,需要减去
num.push(x); // 数字入栈
}
// ② 左括号无优先级,入栈
else if (s[i] == '(')
op.push(s[i]);
// ③ 右括号时,需计算最近一对括号里面的值
else if (s[i] == ')') {
// 从栈中向前找,一直找到左括号
while (op.top() != '(') eval(); // 将左右括号之间的计算完,维护回栈里
// 左括号出栈
op.pop();
} else { // ④ 运算符
// 如果待入栈运算符优先级低,则先计算
while (op.size() && g[op.top()] >= g[s[i]]) eval();
op.push(s[i]); // 操作符入栈
}
}
while (op.size()) eval(); // ⑤ 剩余的进行计算
printf("%d\n", num.top()); // 输出结果
return 0;
}
```
**中转后,后求值版本**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 测试用例:
// (23+124)*(98-2)
// 中缀转后缀
unordered_map<char, int> g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 操作符优化级
string infixToPostfix(string s) {
stack<char> stk; // 栈
string t; // 结果串
for (int i = 0; i < s.size(); i++) {
// ①数字
if (isdigit(s[i])) {
int x = 0;
while (i < s.size() && isdigit(s[i])) {
x = x * 10 + s[i] - '0';
i++;
}
i--;
t += to_string(x) + " ";
} else if (isalpha(s[i])) // ②字符,比如a,b,c
t.push_back(s[i]), t.push_back(' ');
else if (s[i] == '(') // ③左括号
stk.push(s[i]); // 左括号入栈
else if (s[i] == ')') { // ④右括号
while (stk.top() != '(') { // 让栈中元素(也就是+-*/和左括号)一直出栈,直到匹配的左括号出栈
t.push_back(stk.top()), t.push_back(' ');
stk.pop();
}
stk.pop(); // 左括号也需要出栈
} else {
// ⑤操作符 +-*/
while (stk.size() && g[s[i]] <= g[stk.top()]) {
t.push_back(stk.top()), t.push_back(' ');
stk.pop();
}
stk.push(s[i]); // 将自己入栈
}
}
// 当栈不为空时,全部输出
while (stk.size()) {
t.push_back(stk.top()), t.push_back(' ');
stk.pop();
}
return t;
}
// 判断字符是否是操作符
bool isOp(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
// 后缀表达式求值
int eval(string s) {
stack<int> stk; // 后缀表达式求值要使用到一个栈
// 构造一个字符串流,方便处理带空格的后缀表达式
stringstream s1(s);
string c;
while (s1 >> c) {
if (isOp(c[0])) { // 如果是操作数,将其转换为整数并入栈
int a = stk.top();
stk.pop();
int b = stk.top();
stk.pop();
if (c == "+") stk.push(b + a);
if (c == "-") stk.push(b - a);
if (c == "*") stk.push(b * a);
if (c == "/") stk.push(b / a);
} else
stk.push(stoi(c));
}
return stk.top();
}
int main() {
string s;
cin >> s;
// 中缀转后缀
s = infixToPostfix(s);
// 后缀表达式求值
cout << eval(s) << endl;
return 0;
}
```
#### 5、表达式求值(逻辑)
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
/*
0&(1|0)|(1|1|1&0)
答案:1
(0|1&0|1|1|(1|1))&(0&1&(1|0)|0|1|0)&0
答案:0
*/
unordered_map<char, int> g{{'|', 1}, {'&', 2}};
stack<int> num;
stack<char> op;
void eval() {
int a = num.top();
num.pop();
int b = num.top();
num.pop();
char p = op.top();
op.pop();
int r;
if (p == '|') r = b | a;
if (p == '&') r = b & a;
num.push(r);
}
int main() {
string s;
cin >> s;
for (int i = 0; i < s.size(); i++) {
if (isdigit(s[i]))
num.push(s[i] - '0');
else if (s[i] == '(')
op.push(s[i]);
else if (s[i] == ')') {
while (op.top() != '(') eval();
op.pop();
} else {
while (op.size() && g[op.top()] >= g[s[i]]) eval();
op.push(s[i]);
}
}
while (op.size()) eval();
printf("%d\n", num.top());
return 0;
}
```
#### [6、$AcWing$ $1623$. 二叉表达式树转中缀表达式](https://www.acwing.com/problem/content/1625/)
> - 给定一个**二叉表达式树**,请你输出相应的**中缀表达式**,并利用括号反映运算符的优先级。
>**$dfs$中序遍历** 可以求中缀表达式的值,仔细观察用例,可以知道根不是任何人的儿子,需要记录入度找到入度为$0$的点,就是根
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 30;
/*
思路
1、数据处理,通过记录入度,找到入度为0的点,找到根节点
2、二元运算必须有符号右边那个式子,即根据数据构建完树后的右孩子
故在递归遍历树的时候,只要判断该节点是否有孩子,
然后返回的字符串数: 递归左子树的string值 + 该节点的string值 + 递归右子树的string值
3、如果该节点不是根节点,则在该返回值的左右两边加上括号
4、最后输出返回值
黄海总结:
1、乍一看此题,感觉无从下手,因为不知道每个点都是什么节点号!后来慢慢从测试用例1中发现,
data 都是什么 *,a,b,-,c之类,不是节点号,应该是具体的操作符,也就是值。
那节点号在哪里呢?再看一下后面的left_child,right_child,发现是
8 7
-1 -1
4 1
2 5
这个来理解一下,应该是指8号节点,7号节点,-1代表的是NULL节点,也就是到头了,是叶子。
再来仔细看一下,发现出现的数字是:1 2 4 5 6 7 8,单单缺少了一个3!
为啥缺少3呢?
因为3不是任何人的left_child,right_child,它是根!!!!,这样的话,整个图就完整了!
*/
int n;
string w[N];
int l[N], r[N], in[N];
int isleaf[N];
/*
8
* 8 7
a -1 -1
* 4 1
+ 2 5
b -1 -1
d -1 -1
- -1 6
c -1 -1
(a+b)*(c*(-d))
*/
string dfs(int u) {
// 表达式树特点:叶子节点都是真实数字,非叶子节点都是操作符
string left, right;
// if (isleaf[u]) return w[u]; // 递归终点,如果是叶子,本句话可以省略掉
// 这是因为isleaf[u]时,l[u]==-1 && r[u]==-1 ,则下面的两个分支都不会走,直接到了return那句,left,right都是空,还是返回w[u],一样的
if (l[u] != -1) {
left = dfs(l[u]); // 递归左儿子,
if (!isleaf[l[u]]) left = "(" + left + ")"; // 如果左儿子不是叶子,需要把表达式加上括号
}
if (r[u] != -1) { // 右儿子不为空
right = dfs(r[u]); // 递归右儿子
if (!isleaf[r[u]]) right = "(" + right + ")"; // 如果右儿子不是叶子,需要把表达式加上括号
}
// 返回中序遍历结果
return left + w[u] + right;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> l[i] >> r[i]; // data left_child right_child
if (l[i] != -1) in[l[i]]++; // 记录入度
if (r[i] != -1) in[r[i]]++; // 记录入度
if (l[i] == -1 && r[i] == -1) isleaf[i] = 1; // 叶子
}
// 找根
int root;
for (root = 1; root <= n; root++)
if (!in[root]) break; // 入度为零的是根
// 通过中序遍历,构建中缀表达式
cout << dfs(root) << endl;
return 0;
}
```
#### [7、$AcWing$ $4274$. 二叉表达式树转后缀表达式](https://www.acwing.com/problem/content/4277/)
> - 给定一个**二叉表达式树**,请你输出相应的**后缀表达式**,要求使用括号反映运算符的优先级。
>**$dfs$后序遍历** 可以求后缀表达式的值,仔细观察用例,可以知道根不是任何人的儿子,需要记录入度找到入度为$0$的点,就是根
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 30;
int n;
string w[N];
int l[N], r[N], in[N];
int isleaf[N];
/*
题目描述:
给出中缀表达式的语法树 请你将后缀表达式输出 括号体现优先级 操作符不可用括号包含。
思路:
后缀表达式就是把运算符后置的表达式,可以抽象成一棵二叉树,其中的的节点有两种情况:
(1)有左右儿子
(2)只有右儿子
只有右儿子的情况就是负数啦~ 测试用例: - -1 6
然后就是找根,这个很简单,统计所有节点的入度,唯一的一个入度是 0 的节点就是根。
再写个简单的递归就可以了。
8
* 8 7
a -1 -1
* 4 1
+ 2 5
b -1 -1
d -1 -1
- -1 6
c -1 -1
输出样例1:
(((a)(b)+)((c)(-(d))*)*)
*/
string dfs(int u) {
if (isleaf[u]) return "(" + w[u] + ")";
if (l[u] == -1 && r[u] != -1) return "(" + w[u] + dfs(r[u]) + ")"; // 负号
return "(" + dfs(l[u]) + dfs(r[u]) + w[u] + ")"; // 后缀表达式
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> w[i] >> l[i] >> r[i];
if (l[i] != -1) in[l[i]]++;
if (r[i] != -1) in[r[i]]++;
if (l[i] == -1 && r[i] == -1) isleaf[i] = 1; // 叶子
}
// 找根
int root;
for (root = 1; root <= n; root++)
if (!in[root]) break; // 入度为零的是根
cout << dfs(root) << endl;
return 0;
}
```
####[8、$HDU1123$ $Complicated$ $Expressions$](http://acm.hdu.edu.cn/showproblem.php?pid=1123)
> **题意**:
给定一个 **中缀表达式**(只有小写字母,加减乘除,左右括号),**会带有很多冗余的括号**,要求去除这些括号时满足以下的要求:
($1$)、完全没有意义的括号去除;
($2$)、加法和乘法的结合律 -> a+(b+c) 等价于 a+b+c ,a*(b*c) 等价于 a * b * c;故这种括号要去除
($3$)、有明显的不影响运算的括号去除,如a+(b * c) -> a+b*c
**思路:**
先不考虑上述要求,
($1$)对于给定的中缀表达式,先转化成后缀表达式(中缀->后缀);
($2$)然后利用后缀表达式建立一颗二叉树(**后缀二叉树建立表达式树**);
($3$)利用表达式树得到中缀表达式。
做完以上要求后,把要求的括号去除与否的规则考虑进去就可以了
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
// 中缀转后缀
unordered_map<char, int> g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 操作符优先级
string infixToPostfix(string s) {
string t; // 结果串
stack<char> stk; // 栈
for (int i = 0; i < s.size(); i++) {
// ①数字
if (isdigit(s[i])) {
int x = 0;
while (i < s.size() && isdigit(s[i])) {
x = x * 10 + s[i] - '0';
i++;
}
i--;
t += to_string(x), t += ' ';
} else if (isalpha(s[i])) // ②字符,比如a,b,c
t += s[i], t += ' ';
else if (s[i] == '(') // ③左括号
stk.push(s[i]); // 左括号入栈
else if (s[i] == ')') { // ④右括号
while (stk.top() != '(') { // 让栈中元素(也就是+-*/和左括号)一直出栈,直到匹配的左括号出栈
t += stk.top(), t += ' ';
stk.pop();
}
stk.pop(); // 左括号也需要出栈
} else {
// ⑤操作符 +-*/
while (stk.size() && g[s[i]] <= g[stk.top()]) {
t += stk.top(), t += ' ';
stk.pop();
}
stk.push(s[i]); // 将自己入栈
}
}
// 当栈不为空时,全部输出
while (stk.size()) {
t += stk.top(), t += ' ';
stk.pop();
}
return t;
}
// 后缀转表达式树
const int N = 500, M = N << 1;
#define ls e[h[u]] // 左儿子
#define rs e[ne[h[u]]] // 右儿子,由于表达式树是二叉树,有右儿子时一定有左儿子
// 链式前向星
int e[M], h[N], idx, ne[M];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
char a[N];
int al = 25; // a[]:节点号对应的字符,0~25为数字a~z在表达式树中节点的编号,26及以上为每一个操作符的编号,注意:如果前后出现多次同一运算符,它们的节点号是不一样的
// 构造表达式树
int build(string postfix) {
stack<int> stk; // 节点号栈
// 链式前向星初始化
memset(h, -1, sizeof h);
idx = 0, al = 25;
// 后缀表达式->表达式树,可以转化:前缀表达式,中缀表达式,后缀表达式
for (int i = 0; i < postfix.size(); i++) { // 枚举后缀表达式每一个字符
char c = postfix[i];
if (c == ' ') continue;
if (isalpha(c))
stk.push(c - 'a'); // 不是操作符,是数字,按字符-'a'的数字号入栈
else { // 如果是运算符
int x = stk.top(); // 取出两个节点
stk.pop();
int y = stk.top();
stk.pop();
a[++al] = c; // 操作符,申请一个节点号,并且,记录下来节点号与原字符的关系
add(al, x), add(al, y); // 由操作符向左右两个儿子连出边
stk.push(al); // 操作符节点入栈
}
}
return stk.top(); // 栈顶部的字符就是根
}
// 输出节点u
char out(int u) {
if (u >= 26) return a[u]; // 节点号大于等于26的是操作符+-*/
return u + 'a'; // a~z,记录的是0~25,输出需要加上偏移量'a'
}
// 判断操作符
bool isOp(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
// 获取符合题意的中缀表达式
string inorder(int u) {
string ans = "";
if (h[u] == -1) {
ans += out(u);
return ans;
}
string lstr = inorder(ls);
string rstr = inorder(rs);
// 下面的代码是本题的题意要求
if (isOp(a[ls]) && g[a[u]] > g[a[ls]]) { // 如果左儿子是操作符,并且当前运算符优先级大于左儿子运算优先级,需要左儿子加上括号
ans += '(';
ans += lstr;
ans += ')';
} else
ans += lstr; // 否则,左儿子不加括号
ans += out(u); // 左儿子完事,把自己加上
if (isOp(a[rs]) && g[a[rs]] <= g[a[u]]) { // 如果右儿子是操作符,并且,右儿子运算符优先级小于等于当前运算符优先级
if ((a[u] == '+' && a[rs] == '-') // 当前是+,右儿子是-
|| (a[u] == '*' && a[rs] == '/') // 当前是*,右儿子是/
|| (a[u] == '+' && a[rs] == '+') // 当前是+,右儿子是+
|| (a[u] == '*' && a[rs] == '*')) // 当前是*,右儿子是*
ans += rstr; // 都不需要加括号
else
ans += '(' + rstr + ')'; // 否则需要加括号
} else
ans += rstr; // 否则右儿子不加括号
return ans;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("HDU1123.in", "r", stdin);
freopen("HDU1123.out", "w", stdout);
#endif
int n;
cin >> n;
while (n--) {
string infix;
cin >> infix;
// 中缀转后缀
string postfix = infixToPostfix(infix);
// 根据后缀表达式创建表达式树
int root = build(postfix);
cout << inorder(root) << endl;
}
return 0;
}
```
### 三、$TODO$ 以后再做
#### [【洛谷】$P1449$ 后缀表达式](https://www.cnblogs.com/littlehb/p/15074451.html)
#### [【洛谷】$P7073$ [$CSP-J2020$] 表达式](https://www.luogu.com.cn/problem/P7073)
#### [【洛谷】$P8815$ [$CSP-J2022$] 逻辑表达式](https://www.luogu.com.cn/problem/P8815)
#### [【洛谷】$P1981$ $[NOIP2013$ 普及组] 表达式求值](https://www.luogu.com.cn/problem/P1981)
#### [【洛谷】$P1310$ $[NOIP2011$ 普及组] 表达式的值](https://www.luogu.com.cn/problem/P1310)
#### [$AcWing$ $2769$. 表达式](https://www.acwing.com/problem/content/2771/)【中等】
#### [$AcWing$ $151$.表达式计算$4$](https://www.acwing.com/problem/content/153/)【中等】
#### [$AcWing$ $448$. 表达式的值](https://www.acwing.com/problem/content/450/)【中等】
#### [$AcWing$ $454$. 表达式求值](https://www.acwing.com/problem/content/456/)【中等】
#### [$AcWing$ $486$. 等价表达式](https://www.acwing.com/problem/content/488/)【中等】
#### [$AcWing$ $1247$. 后缀表达式](https://www.acwing.com/problem/content/1249/)【中等】
#### [$AcWing$ $1330$. 表达式求值](https://www.acwing.com/problem/content/1332/)【中等】
#### [$AcWing$ $4730$. 逻辑表达式](https://www.acwing.com/problem/content/4733/)【中等】