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.

17 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 4730. 逻辑表达式

一、题目描述

逻辑表达式是计算机科学中的重要概念和工具,包含逻辑值、逻辑运算、逻辑运算优先级等内容。

在一个逻辑表达式中,元素的值只有两种可能:0(表示假)和 1(表示真)。元素之间有多种可能的逻辑运算,本题中只需考虑如下两种:“与”(符号为 &)和“或”(符号为 |)。其运算规则如下:

0 \mathbin{\&} 0 = 0 \mathbin{\&} 1 = 1 \mathbin{\&} 0 = 01 \mathbin{\&} 1 = 1
0 \mathbin{|} 0 = 00 \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 表示待计算的逻辑表达式。

输出格式

输出共两行,第一行输出一个字符 01,表示这个逻辑表达式的值;第二行输出两个非负整数,分别表示计算上述逻辑表达式的过程中,形如 a&ba|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.inexpr/expr3.ans

【样例 #4】

见附件中的 expr/expr4.inexpr/expr4.ans

【数据范围】

\lvert s \rvert 为字符串 s 的长度。

对于所有数据,1 \le \lvert s \rvert \le {10}^6。保证 s 中仅含有字符 01&|() 且是一个符合规范的逻辑表达式。保证输入字符串的开头、中间和结尾均无额外的空格。保证 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 中没有字符 ()

【提示】

以下给出一个“符合规范的逻辑表达式”的形式化定义:

  • 字符串 01 是符合规范的;
  • 如果字符串 s 是符合规范的,且 s 不是形如 (t) 的字符串(其中 t 是符合规范的),那么字符串 (s) 也是符合规范的;
  • 如果字符串 ab 均是符合规范的,那么字符串 a&ba|b 均是符合规范的;
  • 所有符合规范的逻辑表达式均可由以上方法生成。

二、题目解析

本题与 NOIP2013普及组复赛第二题《表达式求值》是亲属关系,

关键词

中缀表达式转后缀表达式,后缀表达式求值

前置试题

AcWing 3302 表达式求值

2013 NOIP普及组】表达式求值

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、中缀表达式转后缀表达式逻辑运算符+精简版本)

:因为逻辑运算,数字只有01,所以,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;
}