题目描述
思路解析
一句话答案:LeetCode 921 括号最少添加:从左扫一遍,( 攒进 open、) 有 open 就抵消否则记 need,扫完 need 加 open 就是最少要补的括号数,时间 O(n)、空间 O(1)。
把括号补成合法,最少补几个
给一个只含 ( 和 ) 的字符串 s,你可以在任意位置插入一个 ( 或 ),问最少插入几次能让它变成有效括号串。有效即每个 ( 右边都配得上一个 )、每个 ) 左边都配得上一个 (。题面例子 s=「()))((」,答案是 4。
先数左右括号的个数差,够不够
既然只是补括号,先数出 ( 和 ) 各有几个、看差多少,好像就够了。可 s=「()))((」里两种各 3 个、个数差是 0,实际却要补 4 个。只数个数把顺序丢了:字符串 )( 里两种也各 1 个,但 ) 在前、( 在后谁也配不上谁,得给前面的 ) 补 (、给后面的 ( 补 ),仍要补 2 个。补几个跟总数无关,看的是括号出现的先后。
一遍扫描:( 攒着、) 先配对再记账
换成从左往右扫一遍,手里拿两个计数器:open 记当前攒着、还没配对的 ( 有几个,need 记遇到过、注定配不上的孤儿 ) 有几个。扫到 ( 就 open 加 1,攒着等右边的 ) 来配;扫到 ),open 大于 0 就抵消一个 (、open 减 1,这对括号自己就合法了,open 已经是 0 就没得配、这个 ) 是孤儿,need 加 1。
扫完后,need 是补不上的孤儿 ),open 是余下没人配的 (。这两笔为什么不能相互抵掉?孤儿 ) 一定落在余下的 ( 左边——若某个 ( 在它左边,扫到那个 ) 时还有 ( 可配,它压根不会变孤儿。左边缺的补左边、右边多的补右边,隔着位置合并不了,所以是相加不是相减。
落地成代码,配对和收尾都拿捏住
代码就是一个 for 循环扫字符串:遇到 ( 就 open 加 1;遇到 ) 先看 open,大于 0 就 open 减 1、否则 need 加 1;结束返回 need 加 open。遇到 ) 得先尝试配对再记账——直接 need 加 1、跳过配对,合法的对子也被算成缺口,答案就偏大。结尾余下的每个 ( 各缺一个 ),只返回 need、漏掉 open 就会少算。
拿「()))((」一格一格算到 4
从头走 s=「()))((」:第 0 位 (,open 变 1。第 1 位 ),open 大于 0,抵消一对,open 减回 0。第 2 位又是 ),open 是 0 没得配,need 变 1。第 3 位还是 ),open 仍 0,need 变 2。第 4 位 (,open 变 1。第 5 位又 (,open 变 2。扫完 need=2、open=2,相加得 4,正是要补的括号数。
再拿 (() 验漏加 open 的坑:两个 ( 让 open 攒到 2,第 2 位 ) 抵消一对、open 减到 1,全程没孤儿、need 一直 0。扫完 need=0、open=1,答案 1——若只返回 need 就成 0,可这个 ( 还缺一个 )。
为什么扫一遍就够快
从左到右只扫一遍字符串,每个字符看一次、做的是加减法,时间 O(n),n 是字符串长度。额外空间只有 open、need 两个整数计数器,不随字符串变长而增多,空间 O(1)。一趟遍历就把要补的括号全数清了。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
遇 ( 就 open+1(攒着等右括号);遇 ) 先看手里有没有 ( 能配——有就 open−1(配掉),没有就 need+1(这个 ) 注定要补一个 ( )。
下面拿 "()))((" 一格一格演。盯住屏幕下方的 open 和 need 两个数怎么随指针变化。
起步 · 指针落在第 0 位:字符串 "()))((" 摊成 6 格。指针停在第 0 位,两个计数器都从 0 起步。
第 0 位 = ( · open+1:第 0 位是 (,把它攒进手里:open 从 0 变 1。它在等右边出现一个 ) 来配对。
指针 → 第 1 位:第 0 位处理完(变灰),指针挪到第 1 位。手里还攒着 1 个没配对的 (。
第 1 位 = ) · 配掉一对:第 1 位是 ),而手里正好有 1 个 (。配对成功:open 减回 0。这两格(绿)就是已经合法的一对。
指针 → 第 2 位:前两格配成一对,封存变灰。指针到第 2 位。注意现在 open=0——手里没有空闲的 ( 了。
第 2 位 = ) · 没得配!:第 2 位又是 ),可手里 open=0,没有 ( 能配它!这个孤儿 ) 只能记账:need 从 0 变 1,将来必须给它补一个 (。
指针 → 第 3 位:第 2 位那个孤儿 ) 记完账(标红),指针到第 3 位。现在 need=1:已经欠了 1 个左括号。
第 3 位 = ) · 又没得配:第 3 位还是 ),open 依旧是 0,又一个孤儿!need 从 1 变 2。现在欠了 2 个左括号。
指针 → 第 4 位:两个孤儿 ) 都记账了(标红),need 累到 2。指针到第 4 位,接下来是两个 (。
第 4 位 = ( · open+1:第 4 位是 (,攒进手里:open 从 0 变 1。它在等右边的 ) 来配,可惜后面没有了——先记着。
指针 → 第 5 位:指针到最后第 5 位。手里攒着 1 个没配对的 (,欠债 need 仍是 2。
第 5 位 = ( · open+1:第 5 位还是 (,再攒一个:open 从 1 变 2。这两个 ((都没有右括号能配,扫完后它们就是「余量」。
扫描结束 · 结算:6 格全扫完。结算:手里余下 open=2 个左括号(绿,各缺一个 ))、need=2 个孤儿右括号(红,各缺一个 ()。
答案 = need + open:把两笔账加起来:欠的 2 个 ( 要补、余的 2 个 ) 也要补,一共补 4 个,字符串才合法。答案就是 4。
再演一例 · s = "(()" 起步:换 "(()" 再走一遍,这次专看雷区②说的结尾余下 open。指针停在第 0 位,计数器归零。
第 0 位 = ( · open+1:第 0 位 (,攒进手里:open = 1。
第 1 位 = ( · open+1:第 1 位又是 (,再攒一个:open = 2。手里现在攒着两个左括号。
第 2 位 = ) · 配掉一对:第 2 位 ),手里有 ( 可配,配掉一对:open 从 2 减到 1。全程没出现孤儿,need 一直是 0。
"(()" 结算 · 答案 1:结算:余下 1 个没配对的 (((第 0 位,标灰),它缺一个 ),所以补 1 个就合法,答案 1。验证了「结尾余下的 open 也要补」。
孤儿 ) 总出现在余下 ( 的左边(位置 2、3 在 4、5 之前),所以一个补在左、一个补在右,谁也救不了谁,只能各补各的——这正是 need+open 不能再少的原因。
雷区实演 · 忘加 open:拿 "(((" 试:全是 (,没有任何孤儿 ),need 一直是 0。要是只返回 need 就得 0——大错!其实 3 个 ( 各缺一个 ),必须补 3 个。这就是为什么结尾要加 open。
边界三连:先想空串(0)、纯左括号(只靠 open)、左右都缺(need+open)三种,代码就稳了。
面试追问:把「为何相加不抵消」和「栈与计数等价」讲清楚,是这题面试的得分点。
参考代码
class Solution: def minAddToMakeValid(self, s): open_ = 0 # 没配对的 ( 数 need = 0 # 必须补 ( 的孤儿 ) 数 for c in s: if c == '(': open_ += 1 elif open_ > 0: # ) 且手里有 ( 可配 open_ -= 1 else: # ) 但没 ( 可配 need += 1 return need + open_复杂度
- 时间复杂度:O(n),从左到右扫一遍字符串,每个字符只看一次,n 为字符串长度
- 空间复杂度:O(1),只用了 open、need 两个整数计数器,不随输入规模增长
易错点
面试追问把动画讲成自己的话
追问为什么 need 和 open 不能互相抵消?
追问能用栈做吗?
追问进阶 LC1541 带权重(每个 ( 要两个 ))怎么变?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
使数组唯一的最小增量
LeetCode 945 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题