1. 题目背景与核心挑战
LeetCode 301题"删除无效的括号"是算法题库中的经典难题,长期位列Hot100热门题目榜单。这道题要求我们从一个包含左右括号和其他字符的字符串中,删除最少数量的无效括号,使得剩余的字符串成为有效的括号组合。有效括号串需要满足:
- 每个左括号必须有对应的右括号
- 括号之间不能交叉嵌套(即")("这样的序列是非法的)
这个问题的难点在于:
- 需要处理任意位置的删除操作
- 可能存在多个合法解(需要全部输出)
- 必须确保删除的括号数量最少
2. 解法思路分析与选择
2.1 暴力搜索法的局限性
最直观的解法是尝试删除每一个可能的括号,然后检查剩余字符串是否有效。对于一个长度为n的字符串,这种暴力方法的时间复杂度是O(n×2ⁿ),当n=20时会有约100万种可能,显然不可行。
2.2 BFS广度优先搜索解法
BFS是更优的选择,因为它可以按删除括号的数量分层搜索:
- 首先检查原始字符串是否有效
- 如果无效,生成所有可能删除一个括号后的子串
- 检查这些子串,将有效的加入结果集,无效的继续生成删除两个括号的子串
- 一旦找到有效解,就停止向下一层搜索(保证删除数量最少)
BFS的时间复杂度最坏情况下仍是O(n×2ⁿ),但实际运行时会提前终止,效率更高。
2.3 DFS深度优先搜索优化
DFS结合剪枝可以达到更好的效果:
- 首先统计需要删除的左右括号数量(遍历字符串)
- 使用DFS尝试删除括号,优先删除能更快使字符串合法的括号
- 通过剪枝跳过重复情况和明显无效的路径
DFS的优势在于可以更灵活地控制搜索顺序,配合剪枝能显著减少搜索空间。
3. BFS解法详细实现
3.1 算法步骤
python复制from collections import deque
def removeInvalidParentheses(s):
def is_valid(t):
cnt = 0
for c in t:
if c == '(': cnt += 1
elif c == ')': cnt -= 1
if cnt < 0: return False
return cnt == 0
queue = deque([s])
visited = set([s])
res = []
found = False
while queue:
current = queue.popleft()
if is_valid(current):
res.append(current)
found = True
if found: continue # 确保只处理当前层
for i in range(len(current)):
if current[i] not in '()': continue
next_str = current[:i] + current[i+1:]
if next_str not in visited:
visited.add(next_str)
queue.append(next_str)
return res if res else ['']
3.2 关键点解析
- 有效性检查函数:
is_valid函数通过计数器判断括号是否平衡 - 队列处理:使用队列实现BFS层级遍历
- 去重机制:
visited集合避免重复处理相同字符串 - 提前终止:一旦找到有效解就停止向下一层搜索
3.3 复杂度分析
- 时间复杂度:最坏O(n×2ⁿ),但实际远小于此
- 空间复杂度:O(n×C(n,m)),其中m是最少删除数量
4. DFS解法优化实现
4.1 算法步骤
python复制def removeInvalidParentheses(s):
res = []
left_remove = right_remove = 0
# 计算需要删除的左右括号数量
for c in s:
if c == '(':
left_remove += 1
elif c == ')':
if left_remove > 0:
left_remove -= 1
else:
right_remove += 1
def dfs(index, left_count, right_count, left_remove, right_remove, path):
if index == len(s):
if left_remove == 0 and right_remove == 0:
res.append(''.join(path))
return
c = s[index]
# 删除当前字符的情况
if (c == '(' and left_remove > 0) or (c == ')' and right_remove > 0):
dfs(index + 1, left_count, right_count,
left_remove - (c == '('),
right_remove - (c == ')'),
path)
# 保留当前字符的情况
path.append(c)
if c not in '()':
dfs(index + 1, left_count, right_count, left_remove, right_remove, path)
elif c == '(':
dfs(index + 1, left_count + 1, right_count, left_remove, right_remove, path)
elif c == ')' and left_count > right_count:
dfs(index + 1, left_count, right_count + 1, left_remove, right_remove, path)
path.pop()
dfs(0, 0, 0, left_remove, right_remove, [])
return list(set(res)) if res else ['']
4.2 优化点分析
- 预处理删除数量:先计算必须删除的左右括号数量,减少搜索空间
- 剪枝策略:
- 遇到连续相同括号时,只删除第一个(避免重复)
- 右括号不能多于左括号时直接剪枝
- 去重处理:最后使用set去除重复解
5. 性能对比与选择建议
5.1 两种方法对比
| 特性 | BFS | DFS优化版 |
|---|---|---|
| 时间复杂度 | 较高 | 较低 |
| 空间复杂度 | 较高 | 较低 |
| 实现难度 | 较简单 | 较复杂 |
| 适用场景 | 字符串较短时 | 字符串较长时 |
| 结果顺序 | 按删除数量递增 | 随机 |
5.2 选择建议
- 面试场景:建议使用BFS,逻辑清晰易于解释
- 竞赛场景:推荐DFS优化版,处理更大输入更高效
- 实际应用:根据输入规模选择,通常DFS更优
6. 常见错误与调试技巧
6.1 典型错误案例
-
遗漏有效解:
- 原因:剪枝条件过于严格
- 修复:确保只在连续相同括号时优化
-
结果重复:
- 原因:不同删除顺序得到相同字符串
- 修复:使用集合存储结果去重
-
超时问题:
- 原因:缺少必要剪枝
- 修复:添加预处理删除数量计算
6.2 调试技巧
-
从小例子开始测试:
- 先测试"()())()"
- 再测试"(a)())()"
- 最后测试复杂案例
-
打印中间状态:
- 在DFS中打印当前路径和删除计数
- 在BFS中打印队列内容
-
边界情况检查:
- 全左括号"((("
- 全右括号")))"
- 空字符串""
- 无括号"abc"
7. 变种问题与扩展思考
7.1 变种问题
- 只返回一个解:只需找到第一个有效解即可返回
- 统计删除方式数量:不输出具体解,只统计有多少种删除方式
- 包含其他括号类型:如{}、[]等,需要检查多种括号嵌套
7.2 扩展思考
- 动态规划解法:尝试用DP记录子问题的解,但状态转移较复杂
- 并行计算优化:BFS的每一层可以并行处理
- 机器学习方法:训练模型预测最可能删除的位置(研究前沿)
在实际刷题过程中,这道题很好地训练了对BFS/DFS的理解和应用能力。我个人的经验是,先写出BFS版本确保正确性,再尝试优化为DFS版本。对于括号类问题,计数法判断有效性是一个通用技巧,可以应用到许多类似问题中。
