## 表达式求值专题 ### 一、理论知识 #### [$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 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 g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 操作符优化级 string t; // 结果串 stack 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 using namespace std; /* 中缀逻辑表达式转后缀逻辑表达式 测试用例: 0&(0|1|0) 答案: 001|0|& */ unordered_map g{{'|', 1}, {'&', 2}}; string s, t; stack 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 using namespace std; // 计算后缀表达式的值 // 判断字符是否是操作符 bool isOp(char c) { return c == '+' || c == '-' || c == '*' || c == '/'; } // 后缀表达式求值 int eval(string s) { stack 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 using namespace std; /* 测试用例I: (2+2)*(1+1) 答案:8 测试用例II: 2+(3*4)-((5*9-5)/8-4) 答案:13 */ stack num; // 数字栈 stack op; // 操作符栈 // 优先级表 unordered_map 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 using namespace std; // 测试用例: // (23+124)*(98-2) // 中缀转后缀 unordered_map g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 操作符优化级 string infixToPostfix(string s) { stack 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 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 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 g{{'|', 1}, {'&', 2}}; stack num; stack 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 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 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 using namespace std; // 中缀转后缀 unordered_map g{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 操作符优先级 string infixToPostfix(string s) { string t; // 结果串 stack 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 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/)【中等】