1. 题目数据 (Problem Metadata)
- 题目类型:传统题
- 题目链接:Problem - 26B - Codeforces
- 时间限制:5 秒
- 内存限制:256 MB
2. 题意简述 (Problem Summary)
给定长度为 ()的括号串 ,仅由 ( 与 ) 组成。求其最长合法括号子序列的长度。合法括号序列定义为可由 () 经有限次嵌套与拼接得到的序列,空串视为合法。
注意是子序列(可删字符、保相对顺序),不是子串(连续)。
3. 朴素解法 (Brute-Force)
最直接的做法是枚举所有子序列(共 个),对每个子序列用栈判断是否合法(),总复杂度 。 时完全不可行。即便改用区间 DP 求"最长合法子序列", 在 下同样超时超内存。
4. 核心解法 (Main Solution)
- 特殊性质:合法括号序列由若干
()对嵌套/拼接而成,长度必为偶数。子序列允许删除字符,因此只需挑出一组能两两配对的(与),且每个)的配对(在它之前即可——所有(在配对意义上是等价的,只有"数量"和"前后位置"起作用。 - 关键突破:一个
)能贡献价值,当且仅当它之前存在尚未被匹配的(。于是采用贪心:从左到右扫描,维护未匹配的(计数left;遇到)且left > 0就配对一次。多余的)直接丢弃(无前置(可配),多余的(在末尾丢弃(无后续)可配)。 - 推导过程:设
res为成功配对的对数,则
扫描每个字符 :
-
(:left++ -
)且left > 0:left--,res++ -
)且left = 0:跳过
5. 正确性证明 (Proof of Correctness)
方法一:交换论证
设贪心算法匹配对数为 ,任意合法子序列的最大匹配对数为 。显然贪心解合法,故 。下面证明贪心策略不会让结果变得更劣,即证明
假设存在最优解在某个扫描位置(遇到 ) 且 left>0)选择跳过该右括号,而贪心选择匹配。由于所有左括号在配对中仅提供“一个可用名额”,彼此等价,跳过当前右括号所保留的那个左括号,至多只能与后续某个右括号再配对一次,获得最多 对,恰好弥补放弃当前右括号所损失的 对,绝不可能带来额外收益。因此将最优解中该处的“跳过”替换为“匹配”,并保持后续配对不变(若这个左括号后续没有被匹配),或将后续匹配前移(相当于去掉了原本匹配这个左括号的右括号),匹配对数必定不会减少,且仍合法。重复替换,可将任意最优解转化为贪心解,故 。综上 。
直观地,把左括号看作“资源”,右括号看作“机会”。每当有闲置资源时,遇上机会就抓住,肯定不会亏——因为就算这次不用,资源留着以后也只能再换一次机会,最多扯平,不可能赚。所以“能配就配”永远是最优策略。
方法二:上界法
定义前缀差
令 , 为全串右括号总数。
上界:设最长 RBS 的长度为 。任一合法括号子序列中左右括号数相等,长度 。若 ,则在前缀 处右括号比左括号多 个,任何合法子序列在该前缀内必须至少丢弃 个右括号,因此最多保留 个右括号;若 ,最多保留 个。总而言之,有
可达性:贪心算法丢弃右括号的时刻恰是 left=0,即当前前缀差达到新的最小值(为负)。整个过程中丢弃的右括号总数恰好等于 ,故保留的右括号数为 。这些保留的右括号都能与之前的左括号配对,构成合法子序列。所以贪心达到上界,必为最优。
直观地,把右括号总数看作“可用需求”。前缀差的最小值 刻画了全局资源最紧缺的程度——如果某段前缀右括号过多,那这些多余的右括号无论如何都要被扔掉。贪心算法只在实在没有左括号时才扔右括号,所以它扔掉的正好是必须扔的最少数,剩下的全部都能配成对,自然就是最长。
两种方法总结对比
| 维度 | 交换论证(方法一) | 上界法(方法二) |
|---|---|---|
| 证明方向 | 从局部决策出发,证明贪心选择不会劣于最优解 | 先给出全局上界,再证明贪心能达到该上界 |
| 数学工具 | 替换、等价比对 | 前缀和、最小值 |
| 直观性 | 强调“能配就配不亏” | 强调“必须扔的数量由前缀缺口决定” |
| 适用性 | 通用性强,适合多数贪心问题 | 更适合有明确“资源缺口”度量的问题 |
| 简洁性 | 逻辑稍复杂,需处理替换后的合法性 | 形式简洁,结论直观 |
两种证明殊途同归,均严格证明了贪心算法的正确性。在实际问题中,上界法更易快速建立整体认知,而交换论证更能体现贪心算法的本质思想。
6. 复杂度分析 (Complexity)
- 时间复杂度:,单次线性扫描, 远在时限内。
- 空间复杂度:(存储整个字符串),未超 256 MB;额外工作变量为 。
- 常数优化:可用
getchar逐字符读入、不存串,将额外空间降至 ,但 已绰绰有余,不必引入额外编码复杂度。可以参考后面给出的替代版本代码。
7. 实现细节与避坑指南 (Implementation Details)
- 边界条件:
- 全
(或全):res = 0,输出 ,代码自然处理。 - :单字符必为 ,正确。
- 形如
)():第一个)因left = 0被跳过,得到()长度 ,正确。
- 全
- 读入:
cin >> s若不加前导空格,则注意索引从0开始。
8. 参考代码 (Reference Code)
1 |
|
9. 补充说明 (Additional Notes)
- 经典区分:本题是"最长合法括号子序列"的贪心模型;与之易混的是"最长合法括号子串"(要求连续),后者需用栈或 DP 求解(如 Codeforces 5C Longest Regular Bracket Sequence - Solution / LeetCode 32 / 洛谷 P1944),复杂度同为 但思路不同。子序列问题之所以能贪心,正源于"可删字符"带来的自由度——任何
)只要前面有富余(就一定值得配对。
替代版本
下面给出逐字符读入、 额外空间的紧凑写法,避免存储整个字符串,适合 极大或对内存敏感的场景。与 §8 主代码复杂度同为 ,差异仅在空间常数与是否封装函数。
1 |
|