[CSP-J 2022] 逻辑表达式

GitHub跳转原题关系图返回列表

先按优先级把中缀表达式建成语法树,再用显式栈按短路语义迭代求值,只统计真正访问到的子树里的短路次数。

OJ: luogu

题目 ID: P8815

难度:普及+/提高

标签:字符串模拟

日期: 2026-06-19 20:55

题意

给出一个只包含 01&|、括号的合法逻辑表达式。

需要输出:

  1. 表达式最终的值
  2. 形如 a&b 的短路次数
  3. 形如 aba|b 的短路次数

题目规定括号优先,& 高于 |,同级从左到右计算。

思路

最直接的办法是按文法递归下降解析,并在解析过程中按短路语义求值。

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

struct Result {
    int value;
    long long short_and;
    long long short_or;
};

static string s;
static int pos_idx;

Result parse_or();
void skip_or();

Result parse_factor() {
    if (s[pos_idx] == '0' || s[pos_idx] == '1') {
        int value = s[pos_idx] - '0';
        ++pos_idx;
        return {value, 0, 0};
    }

    ++pos_idx;  // skip '('
    Result res = parse_or();
    ++pos_idx;  // skip ')'
    return res;
}

void skip_factor() {
    if (s[pos_idx] == '0' || s[pos_idx] == '1') {
        ++pos_idx;
        return;
    }

    ++pos_idx;  // skip '('
    skip_or();
    ++pos_idx;  // skip ')'
}

Result parse_and() {
    Result left_res = parse_factor();

    while (pos_idx < (int)s.size() && s[pos_idx] == '&') {
        ++pos_idx;
        if (left_res.value == 0) {
            ++left_res.short_and;
            skip_factor();
        } else {
            Result right_res = parse_factor();
            left_res.value &= right_res.value;
            left_res.short_and += right_res.short_and;
            left_res.short_or += right_res.short_or;
        }
    }
    return left_res;
}

void skip_and() {
    skip_factor();
    while (pos_idx < (int)s.size() && s[pos_idx] == '&') {
        ++pos_idx;
        skip_factor();
    }
}

Result parse_or() {
    Result left_res = parse_and();

    while (pos_idx < (int)s.size() && s[pos_idx] == '|') {
        ++pos_idx;
        if (left_res.value == 1) {
            ++left_res.short_or;
            skip_and();
        } else {
            Result right_res = parse_and();
            left_res.value |= right_res.value;
            left_res.short_and += right_res.short_and;
            left_res.short_or += right_res.short_or;
        }
    }
    return left_res;
}

void skip_or() {
    skip_and();
    while (pos_idx < (int)s.size() && s[pos_idx] == '|') {
        ++pos_idx;
        skip_and();
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> s;
    pos_idx = 0;
    Result ans = parse_or();
    cout << ans.value << '\n';
    cout << ans.short_and << ' ' << ans.short_or << '\n';
    return 0;
}

brute.cppparseor/parseand/parsefactorparse_or / parse_and / parse_factor 描述整个文法,并在发生短路时用 skipskip_* 函数把右侧子表达式整体跳过。这个版本很适合理解题意和对拍。

正式解的问题在于:表达式长度能到 10610^6,递归解析和递归求值都可能爆栈。

样例语法树

这张图展示样例 0&(1|0)|(1|1|1&0) 对应的主要语法结构:

graph TD
  A["|"] --> B["&"]
  A --> C["|"]
  B --> D["0"]
  B --> E["(1|0)"]
  C --> F["|"]
  C --> G["1&0"]
  F --> H["1"]
  F --> I["1"]

从图里可以看到,短路求值本质上就是“先看左儿子,再决定要不要进右儿子”。 例如左边的 0&(1|0),只要看到左儿子是 0,就能直接确定整棵 & 子树结果为 0,右边整块都不会被访问。 因此右子树内部的任何短路,也都不该被统计。

所以正式解分成两步:

  1. 先用运算符栈按优先级把中缀表达式建成语法树
  2. 再用显式栈模拟递归求值过程

求值时:

  • 先处理左子树
  • 若当前是 & 且左值为 0,统计一次 & 短路并直接返回
  • 若当前是 | 且左值为 1,统计一次 | 短路并直接返回
  • 否则继续处理右子树,再合并最终值

这样既满足短路定义,也避免了深递归。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXL = 1000000 + 5;

static int left_son[MAXL];
static int right_son[MAXL];
static char node_type[MAXL];
static unsigned char node_value[MAXL];
static int node_cnt;

static int value_stack[MAXL];
static char op_stack[MAXL];
static int eval_node_stack[MAXL];
static unsigned char eval_state_stack[MAXL];

int priority_of(char ch) {
    if (ch == '|') {
        return 1;
    }
    if (ch == '&') {
        return 2;
    }
    return 0;
}

int new_node(char ch, int left_id, int right_id) {
    ++node_cnt;
    node_type[node_cnt] = ch;
    left_son[node_cnt] = left_id;
    right_son[node_cnt] = right_id;
    return node_cnt;
}

void apply_once(int &value_top, int &op_top) {
    char op = op_stack[op_top--];
    int right_id = value_stack[value_top--];
    int left_id = value_stack[value_top--];
    value_stack[++value_top] = new_node(op, left_id, right_id);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string s;
    cin >> s;

    int value_top = 0;
    int op_top = 0;

    for (char ch : s) {
        if (ch == '0' || ch == '1') {
            value_stack[++value_top] = new_node(ch, 0, 0);
        } else if (ch == '(') {
            op_stack[++op_top] = ch;
        } else if (ch == ')') {
            while (op_top > 0 && op_stack[op_top] != '(') {
                apply_once(value_top, op_top);
            }
            --op_top;
        } else {
            while (op_top > 0 && op_stack[op_top] != '(' &&
                   priority_of(op_stack[op_top]) >= priority_of(ch)) {
                apply_once(value_top, op_top);
            }
            op_stack[++op_top] = ch;
        }
    }

    while (op_top > 0) {
        apply_once(value_top, op_top);
    }

    int root = value_stack[value_top];
    long long short_and = 0;
    long long short_or = 0;

    int eval_top = 1;
    eval_node_stack[1] = root;
    eval_state_stack[1] = 0;

    // 迭代模拟递归求值,只在真正访问到的子树里统计短路次数。
    while (eval_top > 0) {
        int u = eval_node_stack[eval_top];
        char ch = node_type[u];

        if (ch == '0' || ch == '1') {
            node_value[u] = (ch == '1');
            --eval_top;
            continue;
        }

        if (eval_state_stack[eval_top] == 0) {
            eval_state_stack[eval_top] = 1;
            eval_node_stack[++eval_top] = left_son[u];
            eval_state_stack[eval_top] = 0;
            continue;
        }

        if (eval_state_stack[eval_top] == 1) {
            if (ch == '&' && node_value[left_son[u]] == 0) {
                ++short_and;
                node_value[u] = 0;
                --eval_top;
                continue;
            }
            if (ch == '|' && node_value[left_son[u]] == 1) {
                ++short_or;
                node_value[u] = 1;
                --eval_top;
                continue;
            }

            eval_state_stack[eval_top] = 2;
            eval_node_stack[++eval_top] = right_son[u];
            eval_state_stack[eval_top] = 0;
            continue;
        }

        if (ch == '&') {
            node_value[u] = node_value[left_son[u]] & node_value[right_son[u]];
        } else {
            node_value[u] = node_value[left_son[u]] | node_value[right_son[u]];
        }
        --eval_top;
    }

    cout << (int)node_value[root] << '\n';
    cout << short_and << ' ' << short_or << '\n';
    return 0;
}

复杂度

建树和求值都只会对每个字符或节点做常数次处理,所以总时间复杂度是 O(s)O(|s|),空间复杂度也是 O(s)O(|s|)

总结

这题的关键不是普通表达式求值,而是“短路会让整棵右子树失去访问资格”。把表达式显式建成语法树后,这个限制就会变得很自然。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析