Files
python/TangDou/CSP-J/AcWingTraining/T9/4730.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

17 KiB
Raw Blame History

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$普及组复赛第二题《表达式求值》是亲属关系,

关键词

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

前置试题

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

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