### [$AcWing$ $4730$. 逻辑表达式](https://www.acwing.com/problem/content/description/4733/) ### 一、题目描述 逻辑表达式是计算机科学中的重要概念和工具,包含逻辑值、逻辑运算、逻辑运算优先级等内容。 在一个逻辑表达式中,元素的值只有两种可能:$0$(表示假)和 $1$(表示真)。元素之间有多种可能的逻辑运算,本题中只需考虑如下两种:“与”(符号为 `&`)和“或”(符号为 `|`)。其运算规则如下: $0 \mathbin{\&} 0 = 0 \mathbin{\&} 1 = 1 \mathbin{\&} 0 = 0$,$1 \mathbin{\&} 1 = 1$; $0 \mathbin{|} 0 = 0$,$0 \mathbin{|} 1 = 1 \mathbin{|} 0 = 1 \mathbin{|} 1 = 1$。 在一个逻辑表达式中还可能有括号。规定在运算时,括号内的部分先运算;两种运算并列时,`&` 运算优先于 `|` 运算;同种运算并列时,从左向右运算。 比如,表达式 `0|1&0` 的运算顺序等同于 `0|(1&0)`;表达式 `0&1&0|1` 的运算顺序等同于 `((0&1)&0)|1`。 此外,在 $C++$ 等语言的有些编译器中,对逻辑表达式的计算会采用一种 **短路** 的策略:在形如 `a&b` 的逻辑表达式中,会先计算 `a` 部分的值,如果 $a = 0$,那么整个逻辑表达式的值就一定为 $0$,故无需再计算 `b` 部分的值;同理,在形如 `a|b` 的逻辑表达式中,会先计算 `a` 部分的值,如果 $a = 1$,那么整个逻辑表达式的值就一定为 $1$,无需再计算 `b` 部分的值。 现在给你一个逻辑表达式,你需要计算出它的值,并且统计出在计算过程中,两种类型的 **短路** 各出现了多少次。需要注意的是,如果某处 **短路** 包含在更外层被 **短路** 的部分内则不被统计,如表达式 `1|(0&1)` 中,尽管 `0&1` 是一处 **短路**,但由于外层的 `1|(0&1)` 本身就是一处 **短路**,无需再计算 `0&1` 部分的值,因此不应当把这里的 `0&1` 计入一处 **短路**。 #### 输入格式 输入共一行,一个非空字符串 $s$ 表示待计算的逻辑表达式。 #### 输出格式 输出共两行,第一行输出一个字符 `0` 或 `1`,表示这个逻辑表达式的值;第二行输出两个非负整数,分别表示计算上述逻辑表达式的过程中,形如 `a&b` 和 `a|b` 的“短路”各出现了多少次。 #### 样例输入 #1 ``` 0&(1|0)|(1|1|1&0) ``` #### 样例输出 #1 ``` 1 1 2 ``` #### 样例输入 #2 ``` (0|1&0|1|1|(1|1))&(0&1&(1|0)|0|1|0)&0 ``` #### 样例输出 #2 ``` 0 2 3 ``` **【样例解释 \#1】** 该逻辑表达式的计算过程如下,每一行的注释表示上一行计算的过程: ```plain 0&(1|0)|(1|1|1&0) =(0&(1|0))|((1|1)|(1&0)) //用括号标明计算顺序 =0|((1|1)|(1&0)) //先计算最左侧的 &,是一次形如 a&b 的“短路” =0|(1|(1&0)) //再计算中间的 |,是一次形如 a|b 的“短路” =0|1 //再计算中间的 |,是一次形如 a|b 的“短路” =1 ``` **【样例 \#3】** 见附件中的 `expr/expr3.in` 与 `expr/expr3.ans`。 **【样例 \#4】** 见附件中的 `expr/expr4.in` 与 `expr/expr4.ans`。 **【数据范围】** 设 $\lvert s \rvert$ 为字符串 $s$ 的长度。 对于所有数据,$1 \le \lvert s \rvert \le {10}^6$。保证 $s$ 中仅含有字符 `0`、`1`、`&`、`|`、`(`、`)` 且是一个符合规范的逻辑表达式。保证输入字符串的开头、中间和结尾均无额外的空格。保证 $s$ 中没有重复的括号嵌套(即没有形如 `((a))` 形式的子串,其中 `a` 是符合规范的逻辑表 达式)。 | 测试点编号 | $\lvert s \rvert \le$ | 特殊条件 | | :----------: | :-------------------: | :------: | | $1 \sim 2$ | $3$ | 无 | | $3 \sim 4$ | $5$ | 无 | | $5$ | $2000$ | 1 | | $6$ | $2000$ | 2 | | $7$ | $2000$ | 3 | | $8 \sim 10$ | $2000$ | 无 | | $11 \sim 12$ | ${10}^6$ | 1 | | $13 \sim 14$ | ${10}^6$ | 2 | | $15 \sim 17$ | ${10}^6$ | 3 | | $18 \sim 20$ | ${10}^6$ | 无 | 其中: 特殊性质 1 为:保证 $s$ 中没有字符 `&`。 特殊性质 2 为:保证 $s$ 中没有字符 `|`。 特殊性质 3 为:保证 $s$ 中没有字符 `(` 和 `)`。 **【提示】** 以下给出一个“符合规范的逻辑表达式”的形式化定义: - 字符串 `0` 和 `1` 是符合规范的; - 如果字符串 `s` 是符合规范的,且 `s` 不是形如 `(t)` 的字符串(其中 `t` 是符合规范的),那么字符串 `(s)` 也是符合规范的; - 如果字符串 `a` 和 `b` 均是符合规范的,那么字符串 `a&b`、`a|b` 均是符合规范的; - 所有符合规范的逻辑表达式均可由以上方法生成。 ### 二、题目解析 本题与 $NOIP2013$普及组复赛第二题《**[表达式求值](https://www.acwing.com/problem/content/456/)**》是亲属关系, #### 关键词 中缀表达式转后缀表达式,后缀表达式求值 #### 前置试题 [$AcWing$ $3302$ 表达式求值](https://www.acwing.com/problem/content/3305/) [【$2013$ $NOIP$普及组】表达式求值](http://ybt.ssoier.cn:8088/problem_show.php?pid=1962) #### 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/38-5)+4) 答案: 24 9 6 38 / + 5 - * 4 + */ unordered_map h{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 运算符级别表 string s; // 源串 string t; // 目标串 stack stk; // 利用一个栈,完成中缀表达式转后缀表达式 int main() { cin >> 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--; // 走多了才能停止掉上面的while,需要回退一下 t.append(to_string(x)); } else if (isalpha(s[i])) // ②字符,比如a,b,c t.push_back(s[i]); else if (s[i] == '(') // ③左括号 stk.push(s[i]); // 左括号入栈 else if (s[i] == ')') { // ④右括号 while (stk.top() != '(') { // 让栈中元素(也就是+-*/和左括号)一直出栈,直到匹配的左括号出栈 t.push_back(stk.top()); stk.pop(); } stk.pop(); // 左括号也需要出栈 } else { // ⑤操作符 +-*/ while (stk.size() && h[s[i]] <= h[stk.top()]) { // 哪个操作符优先级高就先输出谁 t.push_back(stk.top()); stk.pop(); } stk.push(s[i]); // 将自己入栈 } } // 当栈不为空时,全部输出 while (stk.size()) { t.push_back(stk.top()); stk.pop(); } // printf("%s", t.c_str()); cout << t << endl; return 0; } ``` #### 2、中缀表达式转后缀表达式(逻辑运算符+拷贝四则版本) ```cpp {.line-numbers} #include using namespace std; /* 中缀的逻辑表达式 转 后缀的逻辑表达式 测试用例: 0&(0|1|0) 答案: 001|0|& */ unordered_map h{{'|', 1}, {'&', 2}}; string s; string t; stack stk; int main() { cin >> 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.append(to_string(x)); } else if (isalpha(s[i])) t.push_back(s[i]); else if (s[i] == '(') stk.push(s[i]); else if (s[i] == ')') { while (stk.top() != '(') { t.push_back(stk.top()); stk.pop(); } stk.pop(); } else { while (stk.size() && h[s[i]] <= h[stk.top()]) { t.push_back(stk.top()); stk.pop(); } stk.push(s[i]); } } while (stk.size()) { t.push_back(stk.top()); stk.pop(); } printf("%s", t.c_str()); return 0; } ``` #### 3、中缀表达式转后缀表达式(逻辑运算符+精简版本) > **注**:因为逻辑运算,数字只有$0$和$1$,所以,`while`循环读取数字没用了,可以省略,当然,你非得要背模板写成一样的,也没有问题。 ```cpp {.line-numbers} #include using namespace std; /* 中缀的逻辑表达式 转 后缀的逻辑表达式 测试用例: 0&(0|1|0) 答案: 001|0|& */ unordered_map h{{'|', 1}, {'&', 2}}; string s; string t; stack stk; int main() { cin >> s; for (int i = 0; i < s.size(); i++) { if (isdigit(s[i]) || isalpha(s[i])) t.push_back(s[i]); else if (s[i] == '(') stk.push(s[i]); else if (s[i] == ')') { while (stk.top() != '(') { t.push_back(stk.top()); stk.pop(); } stk.pop(); } else { while (stk.size() && h[s[i]] <= h[stk.top()]) { t.push_back(stk.top()); stk.pop(); } stk.push(s[i]); } } while (stk.size()) { t.push_back(stk.top()); stk.pop(); } printf("%s", t.c_str()); return 0; } ``` #### 5、中缀表达式求值(四则版本) ```cpp {.line-numbers} // OJ 测试: // AcWing 3302. 表达式求值 // https://www.acwing.com/problem/content/3305/ #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 h{{'+', 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; else if (p == '-') r = b - a; else if (p == '*') r = b * a; else 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])) { //读出完整的数字 int x = 0; while (i < s.size() && isdigit(s[i])) { 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() && h[op.top()] >= h[s[i]]) eval(); op.push(s[i]); //操作符入栈 } } while (op.size()) eval(); //⑤ 剩余的进行计算 printf("%d\n", num.top()); //输出结果 return 0; } ``` #### 6、中缀表达式求值(逻辑表达式+拷贝四则版本) ```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 h{{'|', 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; else 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])) { int x = 0; while (i < s.size() && isdigit(s[i])) { 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() && h[op.top()] >= h[s[i]]) eval(); op.push(s[i]); } } while (op.size()) eval(); printf("%d\n", num.top()); return 0; } ``` #### 7、中缀表达式求值(逻辑表达式+简化版本) ```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 h{{'|', 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; else 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() && h[op.top()] >= h[s[i]]) eval(); op.push(s[i]); } } while (op.size()) eval(); printf("%d\n", num.top()); return 0; } ``` 铺垫的知识完成,现在开始分析本题: * **中缀逻辑表达式求值** * **记录短路次数** #### 规律总结 用一个三元组来替换原版本放在栈里的$int$,即: $Node(v,a,b)$,代表:当前数字值是$v$,已经计算过的$\&$短路次数是$a$,已经计算过的$|$短路次数是$b$ 找一个思路相似的简单问题给大家看看: ![](http://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/2022/12/a1eadd854acc22e84a64cd31e8a4a139.jpg) 则有下面的递推式: $\large (1,a_1,b_1) | (?,a_2,b_2) \Rightarrow (1,a_1,b_1+1) 发生了短路运算,后面的不再计算$ $\large (0,a_1,b_1) | (?,a_2,b_2) \Rightarrow (?,a_1+a_2,b_1+b_2) 没有发生短路计算$ $\large (1,a_1,b_1) \& (?,a_2,b_2) \Rightarrow (?,a_1+a_2,b_1+b_2) 没有发生短路计算$ $\large (0,a_1,b_1) \& (?,a_2,b_2) \Rightarrow (0,a_1+1,b_1) 发生了短路运算,后面的不再计算$ **实现代码**: ```cpp {.line-numbers} /* 测试用例1: 0&(1|0)|(1|1|1&0) 答案: 1 1 2 测试用例2: (0|1&0|1|1|(1|1))&(0&1&(1|0)|0|1|0)&0 答案: 0 2 3 */ #include using namespace std; struct Node { int v, a, b; // v:代表当前的结果值,a: &短路的次数 b:|短路的次数 }; stack num; stack stk; unordered_map h{{'|', 1}, {'&', 2}}; void eval() { // 这里要注意从栈中弹出元素的顺序,先出来的是y右子树,后出来的是x左子树 Node y = num.top(); num.pop(); Node x = num.top(); num.pop(); char p = stk.top(); stk.pop(); Node r; if (p == '|') { if (x.v == 1) r = {x.v, x.a, x.b + 1}; else r = {y.v, x.a + y.a, x.b + y.b}; } else { if (x.v == 1) r = {y.v, x.a + y.a, x.b + y.b}; else r = {x.v, x.a + 1, x.b}; } num.push(r); } int main() { string s; cin >> 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--; num.push({x, 0, 0}); } else if (s[i] == '(') stk.push(s[i]); else if (s[i] == ')') { while (stk.top() != '(') eval(); stk.pop(); } else { while (stk.size() && h[stk.top()] >= h[s[i]]) eval(); stk.push(s[i]); } } while (stk.size()) eval(); printf("%d\n", num.top().v); printf("%d %d\n", num.top().a, num.top().b); return 0; } ```