删除无效的括号 图解题解
这道题到底在问什么
- 输入
- s = "()())()"
- 输出
- ["(())()","()()()"]
- 输入
- s = "(a)())()"
- 输出
- ["(a())()","(a)()()"]
- 输入
- s = ")("
- 输出
- [""]
先想最直接的笨办法
硬枚举所有删除组合?会爆炸,而且分不清谁删得最少。(动画第 3 步)
最优解:一步一步想明白
- 3硬枚举所有删除组合?会爆炸,而且分不清谁删得最少。
- 4关键一句话:BFS 的层数就是删除次数,第一层出现合法串,就是删得最少。
- 5level = { "()())()" }起点:第 0 层只有原串本身,代表「一个都没删」。先看它合不合法。
- 6valid("()())()")判合法只要一个计数器。左括号加一、右括号减一。扫到下标 4 这个右括号,balance 掉到负一,右括号提前多出来,非法。
- 7for i,ch in enumerate(t): nxt.add(t[:i]+t[i+1:])第 1 层怎么来?把原串每一位括号轮流删掉一次,得到七个新串。删第 3 位和第 4 位都得到同一个,所以要去重。
- 8level = nxt (去重后 6 个)去重后第 1 层剩六个候选,每个都只删了一个括号。现在逐个检查它们合不合法。
- 9valid("(())()") → True检查候选 1。它的 balance 一路非负、最后回到零,合法。第一次命中,收进答案。
- 10valid("()()()") → True再看候选 3,也合法。题目要所有最少删除的结果,所以同一层合法的都收,现在答案有两个。
- 11ans = [t for t in level if valid(t)]剩下四个候选 balance 会变负,非法。本层已有答案,它们不必再删一个往第 2 层走了。
- 12if ans: return ans第 1 层已经凑出合法串,直接返回这两个。继续到第 2 层只会删得更多,没必要。
- 13原串 → 删 1 个 → 第一层命中慢放整条链:原串非法,删一个生成六个候选,候选 1 和 3 合法就停。删除次数正好是层数,等于一。
- 14s = ")("看个极端例子。右括号在前的 )( 怎么删都得删两个:第 1 层只剩单个括号还是非法,第 2 层删成空串才合法。空串是合法括号串。
- 17记住这句:最少操作还要所有最短解,BFS 分层是最自然的建模。
- 23把它练到能复述后,去 ds 图论专题里迁移:换个「一步改动」的定义,同一套 BFS 分层就能套到单词接龙这类题。
- 24BFS 是入门款。真正面试想拿满分,去研究「配额 DFS + 剪枝」这套快解。
⚠️ 容易写错的地方
✗ 错:找到一个合法串就立刻 return 它
✓ 对:收集同一层里所有合法串再返回
题目要返回所有删除次数最少的结果,不是任意一个。
✗ 错:找到答案后还继续展开下一层
✓ 对:本层一旦有 ans 就立即 return
下一层多删一个括号,删除次数变多,不再是最少。
✗ 错:生成下一层时把字母也删了
✓ 对:只对 ( 和 ) 做删除
字母不影响合法性,删字母纯属浪费、还会漏掉真正的最少删除解。
✗ 错:不去重,直接用 list 存候选
✓ 对:用 set 存每层候选
删不同位置常生成同一个串(如删第3位、第4位都得 ()()()),不去重会指数级重复扩展。
完整代码(Python / C++ / Java)
Python
class Solution:
def removeInvalidParentheses(self, s):
def valid(t):
bal = 0
for ch in t:
if ch == '(':
bal += 1
elif ch == ')':
bal -= 1
if bal < 0:
return False
return bal == 0
level = {s}
while True:
ans = [t for t in level if valid(t)]
if ans:
return ans
nxt = set()
for t in level:
for i, ch in enumerate(t):
if ch in '()':
nxt.add(t[:i] + t[i + 1:])
level = nxtC++
class Solution {
bool valid(const string& t) {
int bal = 0;
for (char ch : t) {
if (ch == '(') bal++;
else if (ch == ')') {
if (--bal < 0) return false;
}
}
return bal == 0;
}
public:
vector<string> removeInvalidParentheses(string s) {
unordered_set<string> level{s};
while (true) {
vector<string> ans;
for (auto& t : level) if (valid(t)) ans.push_back(t);
if (!ans.empty()) return ans;
unordered_set<string> nxt;
for (auto& t : level)
for (int i = 0; i < (int)t.size(); i++)
if (t[i] == '(' || t[i] == ')')
nxt.insert(t.substr(0, i) + t.substr(i + 1));
level = move(nxt);
}
}
};Java
class Solution {
private boolean valid(String t) {
int bal = 0;
for (char ch : t.toCharArray()) {
if (ch == '(') bal++;
else if (ch == ')' && --bal < 0) return false;
}
return bal == 0;
}
public List<String> removeInvalidParentheses(String s) {
Set<String> level = new HashSet<>();
level.add(s);
while (true) {
List<String> ans = new ArrayList<>();
for (String t : level) if (valid(t)) ans.add(t);
if (!ans.isEmpty()) return ans;
Set<String> nxt = new HashSet<>();
for (String t : level)
for (int i = 0; i < t.length(); i++) {
char ch = t.charAt(i);
if (ch == '(' || ch == ')')
nxt.add(t.substring(0, i) + t.substring(i + 1));
}
level = nxt;
}
}
}套路模板 · 状态空间 BFS(挖空骨架)
# 通用:最少操作 + 返回所有最短解
from collections import deque
def min_ops_all(start):
level = { start } # 第 0 层
while level:
ans = [s for s in level if is_goal(s)] # ← 填:本层达标的状态
if ans:
return ans # 第一层命中即最少
nxt = set() # set 去重是命门
for s in level:
for ns in neighbors(s): # ← 填:一步改动能到的状态
nxt.add(ns)
level = nxt # 进入下一层 = 多操作一次
return []// 通用:最少操作 + 返回所有最短解
vector<string> minOpsAll(const string& start) {
unordered_set<string> level{start}; // 第 0 层
while (!level.empty()) {
vector<string> ans;
for (auto& s : level)
if (isGoal(s)) ans.push_back(s); // ← 填:达标判定
if (!ans.empty()) return ans; // 第一层命中即最少
unordered_set<string> nxt; // 去重命门
for (auto& s : level)
for (auto& ns : neighbors(s)) // ← 填:一步改动
nxt.insert(ns);
level = move(nxt);
}
return {};
}// 通用:最少操作 + 返回所有最短解
List<String> minOpsAll(String start) {
Set<String> level = new HashSet<>();
level.add(start); // 第 0 层
while (!level.isEmpty()) {
List<String> ans = new ArrayList<>();
for (String s : level)
if (isGoal(s)) ans.add(s); // ← 填:达标判定
if (!ans.isEmpty()) return ans; // 第一层命中即最少
Set<String> nxt = new HashSet<>(); // 去重命门
for (String s : level)
for (String ns : neighbors(s)) // ← 填:一步改动
nxt.add(ns);
level = nxt;
}
return Collections.emptyList();
}复杂度
时间复杂度
O(2^n · n)
n 为串长。最坏要枚举大量删除子集(每位删/不删 → 2^n),每个候选 valid 线性扫一遍。题目限制 n ≤ 25 才能跑。
空间复杂度
O(2^n · n)
去重的 set 和当前层最多存指数级个候选串,每串长 O(n)。
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 删除无效的括号 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么 BFS 能保证删除次数最少?+
BFS 按层扩展,第 k 层正好对应删 k 个括号。层数单调递增,第一次出现合法串的那层删除次数必然最小。
能不能不用 BFS,改成 DFS/回溯?+
可以,但要先扫一遍算出左右各该删几个(多余的左括号数、多余的右括号数),DFS 时按这个配额删,并剪枝去重。比 BFS 快很多但更难写,面试可先讲 BFS 思路再提优化方向。
valid 怎么判合法?+
一个计数器 balance:遇左括号加一、遇右括号减一,过程中一旦变负立即非法,扫完必须恰好为 0。字母直接跳过。
时间复杂度怎么估?为什么能过?+
最坏 O(2^n · n),因为每个括号删或不删共 2^n 种组合。题目把串长限制得很小(n ≤ 25),且第一层命中时只展开一层,实际远没到最坏。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。