完全指南:校验、计数、字典序后继与第 k 个序列生成)
文档教程知识库【免费下载链接】cp-algorithmsAlgorithm and data structure articles for https://cp-algorithms.com (based on http://e-maxx.ru)项目地址https://gitcode.com/GitHub_Trending/cp/cp-algorithms点击查看免费下载本文以 cp-algorithms 仓库的 src/combinatorics/bracket_sequences.md 为骨架系统讲解平衡括号序列这一组合学经典主题从定义出发覆盖单/多括号类型的平衡性校验、基于卡特兰数的计数公式与动态规划递推、字典序后继的线性算法、全部序列的生成以及序列的字典序索引与第 k 个序列的构造。读完本文你将掌握五类经典问题的完整推导与可直接复制运行的 C 实现并了解本仓库测试框架如何验证这些实现的正确性。什么是平衡括号序列平衡括号序列balanced bracket sequence是由括号字符组成的字符串向其中插入适当的数字与数学运算后可以构成一个合法的数学表达式。形式化地平衡括号序列可由如下三条规则递归定义空串 $e$ 是平衡括号序列若 $s$ 是平衡括号序列则 $(s)$ 也是平衡括号序列若 $s$ 与 $t$ 都是平衡括号序列则 $st$拼接也是平衡括号序列。例如$(())()$ 是平衡括号序列而 $())($ 不是——后者在插入运算后永远无法构成合法表达式。该定义可以自然推广到多种括号类型的情形只要在第二条规则中把包裹关系推广到配对括号圆括号、方括号、花括号等并保证内外类型正确匹配即可。本文后续的问题均讨论这两种变体只允许一种括号较简单与允许多种括号较复杂。平衡性校验深度计数与栈单括号类型的深度计数器当只存在一种括号时存在极其简洁的线性校验算法。维护一个计数器 $\text{depth}$表示当前未闭合的开括号数量初始 $\text{depth} 0$。从左到右扫描字符串遇到开括号($\text{depth}$ 加一遇到闭括号)$\text{depth}$ 减一。若扫描过程中任意时刻 $\text{depth}$ 变为负数说明闭括号比开括号多前缀不再“半平衡”或扫描结束时 $\text{depth} \neq 0$说明有开括号未闭合则该字符串不是平衡序列否则它是平衡序列。直观理解depth永远不会为负等价于任意前缀中开括号数不少于闭括号数结束时为零等价于全局开闭数量相等。这两条正是合法括号匹配的充要条件。多括号类型的栈匹配当存在多种括号类型时单个计数器不再够用还必须验证闭括号与最近一个未闭合开括号的类型一致。此时用一个栈替代计数器栈中存放所有尚未匹配的开括号遇到开括号将其压入栈顶遇到闭括号检查两个条件栈非空且栈顶元素与当前闭括号类型相同。两者都满足则弹出栈顶任一不满足则字符串不平衡扫描结束时若栈非空同样判定为不平衡。该算法的时间复杂度为 $O(n)$空间复杂度为 $O(n)$栈大小。它就是标准的括号匹配算法也是表达式求值见仓库中 string/expression_parsing.md等场景的基础工具。平衡序列的计数闭式公式卡特兰数仅含一种括号时长度为 $2n$即 $n$ 对括号的平衡序列数量正是第 $n$ 个卡特兰数$$ \frac{1}{n1}\binom{2n}{n} $$卡特兰数序列从 $C_0$ 开始为 $1, 1, 2, 5, 14, 42, 132, 429, 1430, \dots$见 src/combinatorics/catalan-numbers.md。它在组合学中对应大量等价计数问题$n1$ 个叶子节点的满二叉树数、$n1$ 个因子的完全加括号方式、凸 $n2$ 边形的三角剖分数、不越过主对角线的 $(0,0)\to(n,n)$ 单调格点路径数等。若允许 $k$ 种括号类型则每一对括号可以独立选择 $k$ 种类型中的任意一种因此总数为$$ \frac{1}{n1}\binom{2n}{n}k^n $$该公式成立的前提是各括号对之间的类型选择互相独立且不破坏嵌套结构。动态规划递推上述数字也可以直接用动态规划计算。令 $d[n]$ 为 $n$ 对括号的平衡序列总数。注意任意非空平衡序列的第一个字符必然是开括号与之配对的闭括号出现在其后某个位置这对括号内部是一个平衡序列设其有 $i$ 对括号其后也是一个平衡序列有 $n-1-i$ 对括号。因此$$ d[n] \sum_{i0}^{n-1} d[i]\cdot d[n-1-i] $$边界条件为 $d[0]1$。这正是卡特兰数的经典卷积递推式仓库中 src/combinatorics/catalan-numbers.md 给出了对应的 C 实现计算复杂度 $O(n^2)$也可以通过组合公式优化到 $O(n)$。与闭式公式对比即可验证$d[1]d[0]^21$$d[2]d[0]d[1]d[1]d[0]2$$d[3]d[0]d[2]d[1]d[1]d[2]d[0]5$与卡特兰数一致。求字典序后继平衡序列本节针对单括号类型。给定一个平衡序列要求找出按字典序排列时紧随其后的那个平衡序列(视为小于)。核心思想从右向左找到最靠右的、可以安全替换为闭括号的开括号替换后把剩余后缀填充为字典序最小的合法序列。换言之尽量保留更长的前缀不变只重排后缀。直观解释如下设某个位置 $i$ 原本是开括号。把它改成闭括号后位置 $i$ 处的“深度”变化为 $-2$先因闭合减一同时它不再作为开括号保留。只要替换后到该位置为止仍然满足“前缀闭括号数不超过开括号数”即替换后该位置的深度仍然 $\ge 0$那么替换就是合法的且之后一定能补出一个合法的平衡序列。具体扫描方法从右往左遍历同时维护平衡值 $\text{depth}$遇到开括号递减 $\text{depth}$遇到闭括号递增 $\text{depth}$这是后缀的逆扫描视角。当遇到一个开括号且处理该符号后 $\text{depth}0$ 时说明把当前位置改成)后整个前缀依然平衡即为我们要找的最右可替换位置。随后把该位置字符改为)计算右侧剩余需要补的开、闭括号数量并按字典序最小的方式排列先尽量放开括号再放闭括号。若扫描完整个串都没有找到可替换位置说明当前序列本身就是字典序最大的平衡序列不存在后继。仓库中的 C 实现如下src/combinatorics/bracket_sequences.mdbool next_balanced_sequence(string s) { int n s.size(); int depth 0; for (int i n - 1; i 0; i--) { if (s[i] () depth--; else depth; if (s[i] ( depth 0) { depth--; int open (n - i - 1 - depth) / 2; int close n - i - 1 - open; string next s.substr(0, i) ) string(open, () string(close, )); s.swap(next); return true; } } return false; }该函数在 $O(n)$ 时间内求出下一个平衡序列若不存在后继则返回false。代码细节解释后缀剩余长度 $L n-i-1$替换后该位置的深度变为depth循环中先减一depth 0的判定保证替换后深度非负因此后缀还需补 $L - \text{depth}$ 个非平衡字符其中开括号数为 $(L-\text{depth})/2$闭括号数为 $L - \text{open}$开括号全部前置即得到字典序最小的合法后缀。生成全部平衡序列有时需要输出长度为 $2n$ 的全部平衡序列。文档给出了三种思路思路一字典序逐次迭代。从字典序最小的序列 $((\dots(())\dots))$即 $n$ 个(后跟 $n$ 个)出发反复调用上一节的next_balanced_sequence直到返回false即可按字典序依次得到全部 $C_n$ 个序列。思路二STL 暴力枚举。当 $n$ 较小时例如 $n12$可以直接用 C 的next_permutation枚举string(n, () string(n, ))的所有排列逐一用校验算法过滤出平衡序列再对结果排序。此方法的排列总数是 $\binom{2n}{n}$随 $n$ 增长迅速因此只适合小规模。思路三DP 构造。利用计数动态规划中“第一对括号将其内、其后分为两个平衡子序列”的结构进行递归构造具体生成方法见下文“序列索引”与“第 k 个序列”两节。求平衡序列的字典序索引给定一个含 $n$ 对括号的平衡序列求它在所有 $n$ 对括号平衡序列按字典序排列的列表中的序号从 1 开始。辅助数组 $d[i][j]$定义辅助数组 $d[i][j]$$i$ 为半平衡序列的长度所谓半平衡指每个闭括号都有对应的开括号但开括号可以多于闭括号$j$ 为当前平衡值开括号数减闭括号数。$d[i][j]$ 表示满足该参数条件的序列个数。该数组只针对单括号类型计算。边界$d[0][0]1$$d[0][j]0$$j0$。递推时考察序列的最后一个字符若末字符是开括号(则前一状态为 $(i-1, j-1)$若末字符是闭括号)则前一状态为 $(i-1, j1)$。因此$$ d[i][j] d[i-1][j-1] d[i-1][j1] $$当 $j0$ 时 $d[i][j]0$。整个数组可在 $O(n^2)$ 时间内算出$i$ 至多取 $2n$$j$ 至多取 $n$。单括号类型的索引计算用计数器 $\text{depth}$ 表示当前嵌套深度从左到右扫描给定序列若当前字符 $s[i]$ 是(直接 $\text{depth}$ 加一不累计任何序号因为它已经是当前字典序分支中的最小选择若当前字符 $s[i]$ 是)把所有以(开头的、字典序更小的合法结尾计入答案即累加 $d[2n-i-1][\text{depth}1]$然后 $\text{depth}$ 减一。其中 $2n-i-1$ 是当前位置之后的剩余长度$\text{depth}1$ 是若把当前位置换成(后新形成的平衡值。累加结束后再加 1 即可得到从 1 开始的索引。多括号类型的索引计算设共有 $k$ 种括号类型。处理到字符 $s[i]$、在更新 $\text{depth}$ 之前需要遍历所有字典序小于当前字符的括号类型尝试把它们放到当前位置产生新平衡值 $\text{ndepth} \text{depth} \pm 1$并将补全剩余序列的方案数累加入答案$$ d[2n-i-1][\text{ndepth}]\cdot k^{\frac{2n-i-1-\text{ndepth}}{2}} $$该公式的推导分两步先暂时忽略多类型方案数为 $d[2n-i-1][\text{ndepth}]$再考虑 $k$ 种类型的影响。剩余 $2n-i-1$ 个位置中有 $\text{ndepth}$ 个位置的开括号类型已被确定其余 $(2n-i-1-\text{ndepth})/2$ 对括号的类型可任意独立选择故乘上 $k$ 的对应次幂。生成字典序第 k 个平衡序列给定 $n$ 与 $k$要求构造出所有 $n$ 对括号平衡序列中字典序排第 $k$ 个的序列。同样先计算辅助数组 $d[i][j]$长度 $i$、平衡值 $j$ 的半平衡序列个数。单括号类型从左到右逐位决定当前字符维护嵌套深度 $\text{depth}$。在每个位置 $i$ 处判断放置开括号还是闭括号若 $d[2n-i-1][\text{depth}1] \ge k$说明以(开头的分支中包含第 $k$ 个序列则放置(并 $\text{depth}$ 加一否则第 $k$ 个序列必然位于以)开头的分支中令 $k$ 减去 $d[2n-i-1][\text{depth}1]$放置)并 $\text{depth}$ 减一。仓库中的实现src/combinatorics/bracket_sequences.mdstring kth_balanced(int n, int k) { vectorvectorint d(2*n1, vectorint(n1, 0)); d[0][0] 1; for (int i 1; i 2*n; i) { d[i][0] d[i-1][1]; for (int j 1; j n; j) d[i][j] d[i-1][j-1] d[i-1][j1]; d[i][n] d[i-1][n-1]; } string ans; int depth 0; for (int i 0; i 2*n; i) { if (depth 1 n d[2*n-i-1][depth1] k) { ans (; depth; } else { ans ); if (depth 1 n) k - d[2*n-i-1][depth1]; depth--; } } return ans; }实现细节说明DP 表的行范围是 $2n$序列总长列范围是 $n$平衡值上限边界行 $j0$ 与 $jn$ 需要单独处理避免数组越界构造阶段中depth 1 n保证放置(后平衡值不越界若当前分支不足以容纳第 $k$ 个序列则减去该分支大小后转入)分支。整个算法在 $O(n^2)$预计算 $O(n)$构造时间内完成。多括号类型多类型情形只在两点上不同比较分支大小时要把 $d[2n-i-1][\text{ndepth}]$ 乘以 $k^{(2n-i-1-\text{ndepth})/2}$同时要考虑下一个字符可以取不同的括号类型。以下是使用圆括号(/)与方括号[/]两种类型的实现src/combinatorics/bracket_sequences.mdstring kth_balanced2(int n, int k) { vectorvectorint d(2*n1, vectorint(n1, 0)); d[0][0] 1; for (int i 1; i 2*n; i) { d[i][0] d[i-1][1]; for (int j 1; j n; j) d[i][j] d[i-1][j-1] d[i-1][j1]; d[i][n] d[i-1][n-1]; } string ans; int shift, depth 0; stackchar st; for (int i 0; i 2*n; i) { // ( shift ((2*n-i-1-depth-1) / 2); if (shift 0 depth 1 n) { int cnt d[2*n-i-1][depth1] shift; if (cnt k) { ans (; st.push((); depth; continue; } k - cnt; } // ) shift ((2*n-i-1-depth1) / 2); if (shift 0 depth st.top() () { int cnt d[2*n-i-1][depth-1] shift; if (cnt k) { ans ); st.pop(); depth--; continue; } k - cnt; } // [ shift ((2*n-i-1-depth-1) / 2); if (shift 0 depth 1 n) { int cnt d[2*n-i-1][depth1] shift; if (cnt k) { ans [; st.push([); depth; continue; } k - cnt; } // ] ans ]; st.pop(); depth--; } return ans; }这段代码的关键点因为 $k2$公式中的乘数 $2^{\frac{2n-i-1-\text{ndepth}}{2}}$ 直接以位运算 shift实现其中shift (2n-i-1-(depth±1))/2用栈st记录尚未闭合的开括号类型只有当栈顶为(时才能放置)栈顶为[时才能放置]]分支无需再判断分支大小因为它是当前字典序下最后的可选项每个分支若容量不足以容纳第 $k$ 个序列就减去容量并尝试下一个更大的字符选择从而保证字典序正确。仓库中的实现验证与测试该文档中的三份代码均可在仓库测试框架中直接编译验证。测试基础设施如下test/extract_snippets.py扫描src/**/*.md中形如{.cpp filename}的代码块将其内容抽取为test/name.h。本文的三段代码分别生成为next_balanced_brackets_sequence.h、kth_balances_bracket.h与kth_balances_bracket_multiple.htest/test_balanced_brackets.cpp针对单括号类型用next_permutation暴力枚举string(n, () string(n, ))的全部排列并过滤出平衡序列见该文件的generate_all然后断言next_balanced_sequence依次生成的序列与暴力列表完全一致且对最后一个序列返回falsekth_balanced(n, i1)与暴力列表中第 $i1$ 项完全相等。测试覆盖 $n1\dots 10$对于双括号类型generate_all2枚举由 $i$ 对圆括号与 $n-i$ 对方括号组成的所有平衡排列并排序$i0\dots n$再断言kth_balanced2(n, i1)与之逐项相等覆盖 $n1\dots 5$test/test.sh先运行extract_snippets.py生成头文件再用g -stdc17 -fsanitizeundefined -fno-sanitize-recover编译并运行每个测试程序。这意味着本文全部算法不仅理论正确还在仓库的自动化测试中被逐项断言验证。复杂度与适用范围小结问题单括号类型多括号类型复杂度平衡性校验深度计数器栈匹配$O(n)$ 时间$O(1)/O(n)$ 空间序列计数卡特兰数公式$k$ 次幂修正$O(n)$ 或 $O(n^2)$DP字典序后继从右向左扫描—$O(n)$生成全部序列迭代后继 / STL 枚举 / DP 构造同左按类型扩展$O(C_n \cdot n)$ 或排列级序列索引预计算 $d$ 扫描逐类型累加$O(n^2)$ 预处理 $O(n)$ 查询第 $k$ 个序列预计算 $d$ 逐位构造逐类型分支选择$O(n^2)$ 预处理 $O(n)$ 构造注意事项多括号类型的闭式计数公式与索引/构造公式成立的前提是各括号对类型独立选择而多括号的平衡性校验栈匹配与字典序后继并非所有类型组合都适用——本仓库文档给出的后继算法仅针对单括号类型。实际竞赛题目中若出现多类型字典序问题需按本文第 k 个序列的思路在 DP 值上附加类型幂次进行处理。延伸阅读卡特兰数Catalan Numbers平衡括号序列计数的闭式公式来源及其在二叉树、三角剖分、格点路径等组合问题中的等价表述表达式解析Expression parsing栈匹配在表达式求值中的应用组合学目录 中的 Combinatorics 章节二分系数、容斥原理、Burnside 引理等相邻主题全部测试用例test/test_balanced_brackets.cpp与test/extract_snippets.py、test/test.sh可自行运行验证本文全部代码。赞分享文档教程知识库【免费下载链接】cp-algorithmsAlgorithm and data structure articles for https://cp-algorithms.com (based on http://e-maxx.ru)项目地址https://gitcode.com/GitHub_Trending/cp/cp-algorithms点击查看免费下载相关推荐factory_bot 全局序列Global Sequences完全指南定义、生成与底层原理factory_bot 全局序列Global Sequences完全指南定义、生成与底层原理 导读 在测试数据中邮箱、用户名等字段往往需要格式固定但值测试开发工具factory_bot 序列sequence完全指南全局序列与工厂级序列的定义、生成与操作factory_bot 序列sequence完全指南全局序列与工厂级序列的定义、生成与操作 本篇指南聚焦 factory_bot 的两级序列体系——可在多测试开发工具OpenCorePkg macserial 完全指南Apple Mac 序列号与 MLB 的逆向解码、校验与生成原理OpenCorePkg macserial 完全指南Apple Mac 序列号与 MLB 的逆向解码、校验与生成原理 macserial 是 OpenCore固件操作系统嵌入式上一篇从点亮 LED 到联网 OTAESP32 Arduino 开发完整指南下一篇Blender 作为 Python 模块bpy进阶用法、构建配置与限制解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考