17 KiB
AcWing
4730
. 逻辑表达式
一、题目描述
逻辑表达式是计算机科学中的重要概念和工具,包含逻辑值、逻辑运算、逻辑运算优先级等内容。
在一个逻辑表达式中,元素的值只有两种可能: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】
该逻辑表达式的计算过程如下,每一行的注释表示上一行计算的过程:
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
普及组复赛第二题《表达式求值》是亲属关系,
关键词
中缀表达式转后缀表达式,后缀表达式求值
前置试题
1、中缀表达式转后缀表达式(四则运算+以空格隔开)
#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/38-5)+4)
答案:
24 9 6 38 / + 5 - * 4 +
*/
unordered_map<char, int> h{{'+', 1}, {'-', 1}, {'*', 2}, {'/', 2}}; // 运算符级别表
string s; // 源串
string t; // 目标串
stack<char> 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、中缀表达式转后缀表达式(逻辑运算符+拷贝四则版本)
#include <bits/stdc++.h>
using namespace std;
/*
中缀的逻辑表达式 转 后缀的逻辑表达式
测试用例:
0&(0|1|0)
答案:
001|0|&
*/
unordered_map<char, int> h{{'|', 1}, {'&', 2}};
string s;
string t;
stack<char> 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
循环读取数字没用了,可以省略,当然,你非得要背模板写成一样的,也没有问题。
#include <bits/stdc++.h>
using namespace std;
/*
中缀的逻辑表达式 转 后缀的逻辑表达式
测试用例:
0&(0|1|0)
答案:
001|0|&
*/
unordered_map<char, int> h{{'|', 1}, {'&', 2}};
string s;
string t;
stack<char> 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、中缀表达式求值(四则版本)
// OJ 测试:
// AcWing 3302. 表达式求值
// https://www.acwing.com/problem/content/3305/
#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> 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、中缀表达式求值(逻辑表达式+拷贝四则版本)
#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> h{{'|', 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;
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、中缀表达式求值(逻辑表达式+简化版本)
#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> h{{'|', 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;
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
则有下面的递推式:
\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) 发生了短路运算,后面的不再计算
实现代码:
/*
测试用例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 <bits/stdc++.h>
using namespace std;
struct Node {
int v, a, b; // v:代表当前的结果值,a: &短路的次数 b:|短路的次数
};
stack<Node> num;
stack<char> stk;
unordered_map<char, int> 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;
}