对每个子表达式记录取 0/1 的填法数,用调度场双栈按优先级一次扫描、自底向上合并,O(L) 求出表达式取 0 的方案数。
OJ: roj
题目 ID: 3587
难度:普及
标签:栈表达式求值动态规划计数python
创建: 2026-10-02 09:20
更新: 2026-10-07 13:50
前置题目
luogu P1449后缀表达式A 教的后缀求值「遇到运算符弹出栈顶两个操作数、算完把结果压回」被 B 的调度场在结算运算符时照搬为 reduce_top 的弹两操作数合并压回(先弹右再弹左),只是把数值换成 (c0,c1) 计数状态,再叠加 A 未教的优先级/括号解析。roj 1358中缀表达式值(expr)B 的调度场双栈直接复用 A 教的运算符栈按优先级弹栈与括号边界隔离,只把弹出后的求值换成 (c0,c1) 计数合并。roj 1356计算(calc)B 的 parsing 层直接复用 A 教的调度场优先级弹栈规则(栈顶优先级不低于当前就先弹出、括号作栈边界),把 A 的操作数求值栈换成 (c0,c1) 计数栈自底向上合并,额外叠加的是计数 DP 合并公式与隐式横线补位。
题目描述
对 1 位二进制变量定义两种运算:⊕(字符 +)结果为 0 当且仅当两边都为 0(即逻辑或),×(字符 *)结果为 1 当且仅当两边都为 1(即逻辑与);求值顺序为先算括号内、再算 ×、最后算 ⊕,同级从左到右。
给定一个只含 + * ( ) 四种字符的未完成表达式,操作数位置是横线,横线数 = 运算符个数 + 1,每条横线独立填入 0 或 1,求使表达式值为 0 的填法数模 10007。输入共 2 行:第一行为整数 L(0 ≤ L ≤ 100000),第二行为长度为 L 的表达式串;输出一行一个整数。例如输入 L=4、串 +(*)(即 _+(_*_))时,答案为 3。
思路
对每个子表达式只记录 (c0, c1)——使它取 0 / 取 1 的横线填法数,叶子横线是 (1,1);× 合并时 c1=a1·b1,⊕ 合并时 c0=a0·b0,另一维用总方案数 (a0+a1)(b0+b1) 相减(注意题面的 ⊕ 按真值表是逻辑或而非异或)。用调度场双栈按优先级一遍扫描、边扫边合并(该出现操作数时补一条横线 (1,1),括号用栈边界隔离),最后栈底 c0 即答案,时间 O(L)、空间 O(L),不怕深括号嵌套。
参考代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 09:20
# update_at: 2026-10-02 09:32
import sys
MOD = 10007 # 方案数对 10007 取模
PREC = {'+': 1, '*': 2} # 题面规定 × 先算、⊕ 后算,同级从左到右
def reduce_top(opds: list[tuple[int, int]], ops: list[str]) -> None:
"""弹出栈顶运算符,把它的两个操作数 (取0方案数, 取1方案数) 合并后压回。
两个子表达式的横线互不相交,所以合并时方案数直接相乘;
整体方案 = 两边各自全部方案之积,再按运算规则拆成 0/1 两份。
"""
b0, b1 = opds.pop()
a0, a1 = opds.pop()
whole = (a0 + a1) * (b0 + b1) % MOD # 合并后的全部填法
if ops.pop() == '*': # ×(AND):两边都是 1 才得 1
one = a1 * b1 % MOD
opds.append(((whole - one) % MOD, one))
else: # ⊕(OR):两边都是 0 才得 0
zero = a0 * b0 % MOD
opds.append((zero, (whole - zero) % MOD))
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
length = int(next(data)) # 第 1 个 token L:运算符与括号个数,横线都夹在它们之间
s = next(data).decode() # 第 2 个 token:只剩 + * ( ) 的半成品表达式
opds: list[tuple[int, int]] = [] # 操作数栈:(该子表达式为 0 的方案数, 为 1 的方案数)
ops: list[str] = [] # 运算符栈
expect = True # 当前位置该出现操作数(横线或左括号)
for c in s:
# 期待操作数却直接遇到运算符 → 运算符前面藏了一条横线
if expect and c != '(':
opds.append((1, 1)) # 单条横线取 0、取 1 各 1 种填法
expect = False
if c in '+*':
# 栈顶运算符不比当前低级(× 先于 ⊕、同级从左到右)就先弹出来算
while ops and ops[-1] != '(' and PREC[ops[-1]] >= PREC[c]:
reduce_top(opds, ops)
ops.append(c)
expect = True # 运算符后面必然跟一个操作数
elif c == '(':
ops.append(c) # 左括号入栈,等里面的表达式算完再弹
elif c == ')':
while ops[-1] != '(':
reduce_top(opds, ops)
ops.pop() # 去掉左括号,整个括号体作为一个操作数
expect = False
if expect: # 表达式以横线收尾
opds.append((1, 1))
while ops:
reduce_top(opds, ops)
print(opds[0][0])
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 14:43
* update_at: 2026-10-06 14:43
*/
// 按题面真值表:+ 是逻辑或(同为 0 才得 0),* 是逻辑与(同为 1 才得 1)。
// 对每个子表达式记录 (c0, c1):使它取 0 / 取 1 的横线填法数,叶子横线是 (1,1)。
// 用调度场双栈一遍扫描:按优先级弹运算符合并,O(L) 求出整个表达式取 0 的方案数。
#include <cstdio>
#include <string>
#include <vector>
typedef long long ll;
const ll MOD = 10007;
// 合并结果的结构:c[0] 是子表达式取 0 的填法数,c[1] 是取 1 的填法数
struct Node {
ll c0;
ll c1;
};
std::string expr; // 输入的运算符与括号串
std::vector<Node> nums; // 操作数栈:每个元素是一个子表达式的 (c0, c1)
std::vector<char> ops; // 运算符栈:保存 + * ( 三种字符
bool expect_num; // 当前位置是否该出现操作数(横线是隐式的,靠它补上)
// 弹出一个运算符,合并操作数栈顶的两个子表达式
void reduce_top() {
Node b = nums.back(); nums.pop_back();
Node a = nums.back(); nums.pop_back();
ll whole = (a.c0 + a.c1) * (b.c0 + b.c1) % MOD; // 两侧横线互不相交,方案数相乘
Node res;
if (ops.back() == '*') {
// 与:结果为 1 当且仅当两边都是 1
res.c1 = a.c1 * b.c1 % MOD;
res.c0 = (whole - res.c1 + MOD) % MOD;
} else {
// 或:结果为 0 当且仅当两边都是 0
res.c0 = a.c0 * b.c0 % MOD;
res.c1 = (whole - res.c0 + MOD) % MOD;
}
nums.push_back(res);
ops.pop_back();
}
// 判断栈顶运算符优先级是否不低于当前运算符(* 高于 +,同级从左到右)
bool should_reduce(char cur) {
if (ops.back() == '(') return false;
if (ops.back() == '*' && cur == '+') return true;
return ops.back() == cur; // 同级先算左边的
}
int main() {
ll len;
scanf("%lld", &len);
(void)len;
// 表达式串最长 1e5,用静态大缓冲读入
static char big[100005];
scanf("%s", big);
expr = big;
expect_num = true;
for (ll i = 0; i < (ll)expr.size(); i++) {
char ch = expr[i];
if (ch == '(') {
// 左括号直接压栈,括号内自成一层
ops.push_back('(');
expect_num = true;
} else if (ch == ')') {
// 遇右括号:先补上括号内最后一条横线,再弹到左括号为止
if (expect_num) nums.push_back(Node{1, 1});
while (ops.back() != '(') reduce_top();
ops.pop_back(); // 弹掉 '(',括号内结果收成一个操作数
expect_num = false;
} else {
// 是运算符:若该出现操作数,说明前面藏着一条横线
if (expect_num) nums.push_back(Node{1, 1});
// 按优先级先合并栈顶可以算完的
while (!ops.empty() && should_reduce(ch)) reduce_top();
ops.push_back(ch);
expect_num = true;
}
}
// 表达式以横线收尾(如以运算符结束)时补最后一条横线
if (expect_num) nums.push_back(Node{1, 1});
// 弹空运算符栈,最终操作数栈里只剩整个表达式的 (c0, c1)
while (!ops.empty()) reduce_top();
printf("%lld\n", nums.back().c0);
return 0;
}