小欧的括号操作
小欧的括号操作
OPPO 笔试真题 · 2023 秋招 · 算法岗
判题请到牛客原题提交。
题目描述
小欧拿到了一个只包含'('和')'的字符串,她有以下两种操作:
用"("代替一对括号:"()"。
用")"代替一对括号:"()"。
请注意,只有相邻的括号字符才可以操作。
小欧想知道,若干次操作以后,该字符串的最短长度是多少?
输入格式
一个只包含'('和')'两种字符的字符串。长度不超过200000。
输出格式
一个整数,代表若干次操作后,字符串的最短长度。
示例
示例 1
输入:
()输出:
1示例 2
输入:
)(输出:
2解题思路
第一步:理解问题本质
两种操作作用的是同一对相邻括号 (),区别只在于保留哪一个字符:
- 换成
(:等于删掉这对里的),位置由(占住; - 换成
):等于删掉这对里的(,位置由)占住。
不管选哪一种,长度都恰好减少 1。所以「让字符串最短」就是「尽可能多删字符」。
第二步:把操作看成「谁往哪边走」
一次操作要求两个字符相邻、且左边是 (、右边是 )。于是可以这样理解这两个字符:
(只能向右走 —— 它一路把右边的)删掉,自己占住空出来的位置;)只能向左走 —— 它一路把左边的(删掉。
同类字符之间凑不出 (),所以谁也不会越过对方。由此可以直接判断哪些字符永远动不了:
| 位置 | 记作 | 为什么动不了 |
|---|---|---|
第一个 ( 之前的所有 ) | c0 | 左边全是 ),没有 ( 能过来吃它 |
最后一个 ) 之后的所有 ( | o0 | 右边全是 (,没有 ) 能过来吃它 |
如果第一个 ( 已经排在最后一个 ) 的右边(形如 ))((),整串一个 () 都找不到,
一次操作也做不了,答案就是 n。
第三步:中间那段能消到多短
中间那段从第一个 ( 开始、到最后一个 ) 结束,两种字符都有。
下界是 1:每次操作只删一个字符,而这段里最后一个被删的字符,左边只剩
)、
右边只剩(,两个方向都帮不上忙,所以消不空。上界也是 1,构造如下:始终把最右边的
)当作「收割者」。- 找它左边最近的一个
(,让这个(把两者之间的)逐个吃掉(()→(,(不断右移); - 收割者再把这个
(吃掉(()→),收割者左移一格)。
每一轮至少消掉 2 个字符,收割者稳定左移,最终中间只剩它自己。
- 找它左边最近的一个
所以:
第一个 '(' 在最后一个 ')' 左边: 答案 = c0 + o0 + 1
否则: 答案 = n完整代码实现
import sys
def solve() -> None:
"""统计两端动不了的字符,中间的括号段再留下 1 个"""
s = sys.stdin.read().strip()
if not s:
return
n = len(s)
first_open = s.find('(') # 第一个左括号的位置
last_close = s.rfind(')') # 最后一个右括号的位置
# 没有左括号 / 没有右括号 / 全是「先 ) 后 (」:一次操作也做不了
if first_open == -1 or last_close == -1 or first_open > last_close:
sys.stdout.write(str(n) + "\n")
return
# 第一个左括号左边的 ')' 与最后一个右括号右边的 '(' 都动不了
stuck = s.count(')', 0, first_open) + s.count('(', last_close + 1)
# 中间那段既有 '(' 又有 ')',可以一路消到只剩 1 个
sys.stdout.write(str(stuck + 1) + "\n")
if __name__ == "__main__":
solve()示例推演
示例 1:s = "()"
| 步骤 | 内容 |
|---|---|
第一个 ( | 下标 0 |
最后一个 ) | 下标 1 |
左边界之前的 ) | 0 个 |
右边界之后的 ( | 0 个 |
| 答案 | 0 + 0 + 1 = 1 |
实际执行一次操作:() → ((或 )),长度从 2 变成 1,再没有 () 可操作。
示例 2:s = ")("
第一个 ( 在下标 1,最后一个 ) 在下标 0,第一个 ( 在最后一个 ) 的右边,
串里不存在相邻的 (),一次操作都无法进行,答案是 2。
补充例子:s = "(()))("
| 步骤 | 内容 |
|---|---|
第一个 ( | 下标 0 |
最后一个 ) | 下标 4 |
左边界之前的 ) | 0 个 |
右边界之后的 ( | 1 个(下标 5) |
| 答案 | 0 + 1 + 1 = 2 |
中间段 (())) 的消法:
(())) 用下标 1 的 '(' 吃掉下标 2、3 的 ')' → (()
(() 下标 4 的 ')' 吃掉下标 1 的 '(' → ()
() 再把 '(' 吃掉 → )最后和末尾那个动不了的 ( 拼起来,剩 )((两个字符),与公式一致。
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
直接模拟(每次找 () 替换) | O(n²) | O(n) | 长串会超时 |
| 统计两端动不了的字符 | O(n) | O(1) | 推荐,只需四次扫描 |
易错点总结
1. 用「左括号数 − 右括号数」当答案
反例:s = "()(",2 个左括号、1 个右括号,差值 1,但答案是 2。
末尾那个 ( 右边没有任何 ),永远删不掉。
2. 忽略顺序,把 )( 当成一对
)( 里没有相邻的 (),一次操作都做不了,答案是 2(示例 2 就是在考这一点)。
3. 认为中间段能消成 0
每次操作只删一个字符,而中间段最后一个字符的左右两侧都是「帮不上忙」的方向,
所以至少要留 1 个。
4. find / rfind 返回 -1 时没特判
全是 )、全是 ( 的串里,find('(') 或 rfind(')') 会返回 -1,
不判掉就会算出一个奇怪的下标。代码里把「没有左括号」「没有右括号」「顺序反了」三种情况合并判断。
5. 读输入的方式
字符串只含括号、内部没有空格,但结尾有换行,用 sys.stdin.read().strip() 最稳。
扩展思考
- 如果只允许换成
((也就是只能删)):答案不再是这个公式,而要用栈做括号匹配,
能删掉的)就是能被左边某个(匹配掉的那些,答案 = n − 匹配数。
可见「能换成两种字符」这个自由度才是本题公式这么简洁的原因。 - 如果允许对任意不相邻的两个字符操作:那就退化成纯粹的计数问题,答案会更小,
但也失去了「相邻」这个关键约束。 - 本题的「
(只右移、)只左移」视角,和括号匹配里「栈顶配最近的左括号」是同一套直觉。