ARTICLE · 1126190
真题解析:P7914 [CSP-S 2021] 括号序列(区间DP·唯一分解去重·组合计数·前缀和优化)
题目来源:洛谷 P7914 [CSP-S 2021] 括号序列
题目大意
定义“符合规范的超级括号序列”为由字符 (、)、* 组成的字符串,满足以下递归规则(其中 S 表示任意一段长度在 1 到 k 之间、仅由 * 组成的非空串):
• 基础: ()、(S)都是规范的;• 并置:若 A、B规范,则AB、ASB规范;• 包裹:若 A规范,则(A)、(SA)、(AS)规范;• 空串不是规范的超级括号序列。
给定一个正整数 n、一个正整数 k,以及一个长度为 n、字符集为 (、)、*、? 的字符串。其中每个 ? 都可以独立地填成 (、) 或 *。求有多少种填法,使得填好的整个串成为一个符合规范的超级括号序列。答案对 10^9+7 取模。
直观理解:* 只能出现在括号内部,或者夹在两块括号序列之间,所以任何合法串首字符必是 (、尾字符必是 )。
输入输出样例
样例 1 输入:
7 3(*??*??样例 1 输出:
5样例 1 共有 5 种合法填法:(**)*()、(**(*))、(*(**))、(*)**()、(*)(**)。
样例 2 输入:
10 2???(*??(?)样例 2 输出:
19(样例 3、样例 4 的数据在洛谷题目附件 bracket.zip 中,需登录后下载,这里不再列出。)
考点梳理
[i,j] 上,按区间长度递增计算,短区间答案先算好供长区间转移复用 | |
( 匹配其真实对应的 )”为骨架,保证每个合法串只被统计一次,避免重复计数 | |
canL/canR/canStar 三个 0/1 指示表示 ? 可充当的角色,方案数按角色相乘累加 | |
10^9+7 取模,乘法中间量用 long long 承接防溢出,减法后修正负数 | |
bad 前缀和把“一段能否全是星”判定压到 O(1),整体复杂度降到 O(n^3) |
解题思路
一、把递归文法翻译成 DP 状态
观察规则可知,* 只能在括号内出现,或者在两个合法序列之间出现。因此一个合法串必然由若干“块”首尾相接组成,块与块之间可以夹 0 个或多个 *。把“块”定义为最外层一对括号互相匹配的序列 (X),于是:
• 相邻两块紧挨着,对应规则 AB;• 相邻两块之间夹 1..k个*,对应规则ASB。
这个分解是唯一的:块的边界就是第一层括号恢复平衡的位置,给定字符串后,块和夹层星串就唯一确定。把“块 / 序列”拆成两层,是后面去重的关键。
据此在子区间 [i,j] 上定义三套状态(下文变量名与参考代码完全一致):
• V[i][j]:区间[i,j]本身是一个完整规范超级括号序列的方案数;• E[i][j]:区间[i,j]作为“一对括号之间的内容物”,即E = S | V | SV | VS(注意没有SVS,规则里没有(SVS));• CONT[i][j]:区间[i,j]作为“某对括号闭合之后接续的内容”,即CONT = V或S + V,专门处理块与块之间的星串(ASB)。
二、用“首括号匹配”做唯一分解(去重核心)
最朴素的“包裹 + 拼接”写法会把 (()()) 这类串重复计数——它在 A|BC 和 AB|C 两处各被统计一次。解决办法是人为规定一种唯一的分解:
对任一合法串,其首字符 ( 真实匹配的 ) 位置 p 是唯一确定的。以 p 为骨架,内部 [i+1,p-1] 作为内容物 E,其后 [p+1,j] 作为接续 CONT。这样每个合法串恰好落在唯一的 p 上,只被统计一次。
三、三套区间 DP 的定义与含义
V[i][j] 的转移就是“首括号 i 匹配位置 p 的 ),内部取 E[i+1][p-1],其后取 CONT[p+1][j]”,再乘上 canL[i]·canR[p](首尾能否分别是 ( 和 )):
for (int p = i + 1; p <= j; p++) { if (!canR[p]) continue; int ein = (i + 1 > p - 1) ? 1 : E[i + 1][p - 1]; if (!ein) continue; int cont = (p == j) ? 1 : CONT[p + 1][j]; if (!cont) continue; vv = (vv + (ll)canL[i] * ein % MOD * cont) % MOD;}V[i][j] = (int)vv;E[i][j] 表示“一对括号之间的内容物”,由四种互不重叠的形态相加得到:S(纯星)、V(完整序列)、SV(星 + 序列)、VS(序列 + 星)。四种形态首尾字符特征不同,因此同一字符串不会被两种形态同时统计:
ll ee = ((ll)h[i][j] + V[i][j]) % MOD;for (int x = i; x < j; x++) if (h[i][x]) ee = (ee + (ll)h[i][x] * V[x + 1][j]) % MOD;for (int y = i + 1; y <= j; y++) if (h[y][j]) ee = (ee + (ll)V[i][y - 1] * h[y][j]) % MOD;E[i][j] = (int)ee;CONT[i][j] 表示“括号闭合之后接续的内容”,即在 V 前面再接一段星 S,要么直接是 V,要么是 S + V:
ll cc = V[i][j];for (int b = i; b < j; b++) if (h[i][b]) cc = (cc + (ll)h[i][b] * V[b + 1][j]) % MOD;CONT[i][j] = (int)cc;四、V/E/CONT 的转移细节
• V:枚举匹配位置p,要求canR[p]为真,且内部E、后续CONT均非空(p==j时后续为空,视作1)。• E:h[i][j]为真时贡献纯星S;V[i][j]贡献完整序列V;Σ h[i][x]·V[x+1][j]贡献SV;Σ V[i][y-1]·h[y][j]贡献VS。四种互斥,不重不漏。• CONT:V[i][j]本身,加上Σ h[i][b]·V[b+1][j](在V前接一段星S)。
其中 h[i][j] 表示 [i,j] 能否全为 * 且长度不超过 k,用 bad 前缀和 O(1) 判定:若 [i,j] 内不含非 * 字符(即 bad[j]-bad[i-1]==0)且长度 ≤k,则 h[i][j]=1。
五、用前缀和把判定压到 O(1)
bad[i] 记录前缀中“不能作 *”的位置个数(即 canStar 为假的位置)。那么 [i,j] 能否全是星,只取决于 bad[j]-bad[i-1] 是否为 0。h[i][j] 的预处理因此是常数时间,整体转移虽在 [i,j] 上各有一个 O(n) 的内层枚举(枚举 p、x、y、b),但状态总数是 O(n^2),所以总复杂度为 O(n^3)。n=500 时整数运算量级约为 1.25×10^8,C++ 开启 O2 优化可稳定通过。
六、边界与易错点
• 空串不是合法序列,但 ()的内层空要算:当i+1 > p-1(内部为空)时E取1。• 多算 (SVS):规则只有(A)、(SA)、(AS),内部不能同时两侧夹星,否则答案偏大。• ?不是乘3:每个?最终只承担一种角色,用canL/canR/canStar的0/1指示相乘即可。• k限制S长度且S非空:h[i][j]需满足1 ≤ 长度 ≤ k。• 负数修正:凡是“大减小”形式的区间求和,减法后都要 if (sum < 0) sum += MOD;。• 最终答案是 V[1][n]:要求整个串本身就是一个完整规范的超级括号序列。
参考代码
#include <bits/stdc++.h>using namespace std;typedef long long ll;typedef unsigned long long ull;const int MOD = 1000000007;int main(){ ios::sync_with_stdio(false); cin.tie(0); int n, k; if (!(cin >> n >> k)) return 0; string str; cin >> str; vector<int> canL(n + 2, 0), canR(n + 2, 0), canStar(n + 2, 0); for (int i = 1; i <= n; i++) { char c = str[i - 1]; canL[i] = (c == '(' || c == '?'); canR[i] = (c == ')' || c == '?'); canStar[i] = (c == '*' || c == '?'); } vector<int> bad(n + 2, 0); for (int i = 1; i <= n; i++) bad[i] = bad[i - 1] + (canStar[i] ? 0 : 1); vector<vector<int> > h(n + 2, vector<int>(n + 2, 0)); for (int i = 1; i <= n; i++) for (int j = i; j <= n; j++) { int L = j - i + 1; if (L > k) continue; if (bad[j] - bad[i - 1] == 0) h[i][j] = 1; } vector<vector<int> > V(n + 2, vector<int>(n + 2, 0)); vector<vector<int> > E(n + 2, vector<int>(n + 2, 0)); vector<vector<int> > CONT(n + 2, vector<int>(n + 2, 0)); for (int len = 1; len <= n; len++) for (int i = 1; i + len - 1 <= n; i++) { int j = i + len - 1; ll vv = 0; for (int p = i + 1; p <= j; p++) { if (!canR[p]) continue; int ein = (i + 1 > p - 1) ? 1 : E[i + 1][p - 1]; if (!ein) continue; int cont = (p == j) ? 1 : CONT[p + 1][j]; if (!cont) continue; vv = (vv + (ll)canL[i] * ein % MOD * cont) % MOD; } V[i][j] = (int)vv; ll ee = ((ll)h[i][j] + V[i][j]) % MOD; for (int x = i; x < j; x++) if (h[i][x]) ee = (ee + (ll)h[i][x] * V[x + 1][j]) % MOD; for (int y = i + 1; y <= j; y++) if (h[y][j]) ee = (ee + (ll)V[i][y - 1] * h[y][j]) % MOD; E[i][j] = (int)ee; ll cc = V[i][j]; for (int b = i; b < j; b++) if (h[i][b]) cc = (cc + (ll)h[i][b] * V[b + 1][j]) % MOD; CONT[i][j] = (int)cc; } cout << V[1][n] << "\n"; return 0;}复杂度分析
• 时间: O(n^3)。状态总数O(n^2),每个状态上V、E、CONT各有一个O(n)的内层枚举,合计O(n^3)。n=500时整数运算约1.25×10^8次,C++ O2实测远低于1秒的时间限制。• 空间: O(n^2)。V、E、CONT、h四个二维数组,每个n×n,n=500时约4×500^2×4B ≈ 4MB,远低于512MB内存限制。
推荐阅读
从暴力枚举到算法策略:CSP-J 2021插入排序真题实战解析
真题解析:P11232 [CSP-S 2024] 超速检测(运动学公式·二分映射·区间覆盖贪心)
真题解析:P8818 [CSP-S 2022] 策略游戏(博弈论·分类讨论·ST表区间查询)
真题解析:P7913 [CSP-S 2021] 廊桥分配(贪心·优先队列·前缀和)
真题解析:P7915 [CSP-S 2021] 回文(贪心·双端队列·回文构造)
真题解析:P7075 [CSP-S 2020] 儒略日(模拟·日期计算·闰年判断)
真题解析:P5658 [CSP-S 2019] 括号树(模拟·树形DP·栈)
真题解析:P11230 [CSP-J 2024] 接龙(子序列匹配·状态递推·分类讨论)
真题解析:P14362 [CSP-S 2025] 道路修复(最小生成树·子集枚举·并查集·多路归并)
真题解析:P11233 [CSP-S 2024] 染色(线性DP·前缀和·桶数组·同色段·最近出现位置)
真题解析:P9753 [CSP-S 2023] 消消乐(栈模拟·多项式哈希·双哈希·组合计数)
真题解析:P8817 [CSP-S 2022] 假期计划(BFS最短路·枚举优化·候选集剪枝·64位整数)
真题解析:P9754 [CSP-S 2023] 结构体(模拟·内存对齐·嵌套结构体·地址映射)
真题解析:P9755 [CSP-S 2023] 种树(二分答案·等差数列求和·树上逆向贪心·优先队列)
我是欣爸,中学开始学习编程,计算机专业毕业,从事互联网行业软件开发20余年。热爱编程,热爱算法,孩子也很喜欢数学、编程,业余时间辅导孩子学习编程、算法,分享编程算法学习、信奥竞赛经验。