题目分析
给定包含括号和其他字符的字符串,要求删除最少数量的无效括号,返回所有可能的有效结果。关键点:
- 需要找到所有最小删除方案
- 结果需要去重
- 时间复杂度优化挑战
BFS 解法解析
import java.util.*;
class Solution_BFS {
public List<String> removeInvalidParentheses(String s) {
List<String> result = new ArrayList<>();
if (s == null) {
return result;
}
Set<String> visited = new HashSet<>();
Queue<String> queue = new LinkedList<>();
// Initial state
queue.offer(s);
visited.add(s);
boolean found = false; // Flag to indicate if we found the first valid string(s)
while (!queue.isEmpty()) {
int levelSize = queue.size(); // Process level by level
for (int i = 0; i < levelSize; i++) {
String current = queue.poll();
// Check if the current string is valid
if (isValid(current)) {
result.add(current);
found = true; // Mark that we've found solutions at this level
}
// If we already found solutions at this level,
// no need to generate shorter strings (more removals)
if (found) {
continue;
}
// Generate neighbors (remove one parenthesis)
for (int j = 0; j < current.length(); j++) {
char ch = current.charAt(j);
// Only consider removing parentheses
if (ch == '(' || ch == ')') {
// Create the next string by removing the character at index j
String nextStr = current.substring(0, j) + current.substring(j + 1);
// If this neighbor hasn't been visited, add it to the queue and visited set
if (visited.add(nextStr)) { // Set.add() returns true if the element was not already present
queue.offer(nextStr);
}
}
}
}
// If we found valid strings in this level, stop exploring further (deeper levels)
if (found) {
break;
}
}
return result;
}
// Helper function to check if a string has valid parentheses
private boolean isValid(String str) {
int balance = 0;
for (char ch : str.toCharArray()) {
if (ch == '(') {
balance++;
} else if (ch == ')') {
balance--;
}
// If balance drops below zero at any point, it's invalid
if (balance < 0) {
return false;
}
}
// For a valid string, the final balance must be zero
return balance == 0;
}
}
核心思路:
- 逐层遍历可能的删除方案(类似层级遍历)
- 每层代表删除 N 个括号后的所有可能
- 找到有效解后停止向更深层搜索
优势:
- 天然保证找到最小删除次数
- 实现相对直观
劣势:
- 空间复杂度较高(需存储中间状态)
- 可能生成大量无效中间字符串
DFS 解法解析
import java.util.*;
class Solution_DFS {
private Set<String> resultSet = new HashSet<>();
private String originalString;
private int len;
private int minLeftToRemove;
private int minRightToRemove;
public List<String> removeInvalidParentheses(String s) {
this.originalString = s;
this.len = s.length();
this.resultSet.clear(); // Clear for potential multiple calls
// Step 1: Calculate minimum removals needed
int left = 0, right = 0;
for (int i = 0; i < len; i++) {
char ch = s.charAt(i);
if (ch == '(') {
left++;
} else if (ch == ')') {
if (left > 0) {
left--; // Matched a '('
} else {
right++; // Unmatched ')' found, needs removal
}
}
}
this.minLeftToRemove = left; // Remaining unmatched '(' need removal
this.minRightToRemove = right; // Already counted unmatched ')'
// Step 2: Start DFS
dfs(0, 0, 0, minLeftToRemove, minRightToRemove, new StringBuilder());
return new ArrayList<>(resultSet);
}
/**
* DFS function to explore possibilities.
*
* @param index Current index in the original string.
* @param leftCount Count of '(' included in currentPath so far.
* @param rightCount Count of ')' included in currentPath so far.
* @param leftRem Remaining '(' to remove.
* @param rightRem Remaining ')' to remove.
* @param currentPath StringBuilder building the potential result.
*/
private void dfs(int index, int leftCount, int rightCount, int leftRem, int rightRem, StringBuilder currentPath) {
// Base case: Reached the end of the original string
if (index == len) {
// If we removed the exact required number of parentheses and the result is valid
if (leftRem == 0 && rightRem == 0 && leftCount == rightCount) { // leftCount == rightCount implicitly checks validity here
resultSet.add(currentPath.toString());
}
return;
}
// Pruning: If we have already removed too many, stop this path
if (leftRem < 0 || rightRem < 0) {
return;
}
// Pruning: If rightCount exceeds leftCount, this path cannot lead to a valid solution
if (rightCount > leftCount) {
return;
}
char currentChar = originalString.charAt(index);
int pathLength = currentPath.length(); // Store length for backtracking
if (currentChar == '(') {
// Option 1: Remove '('
dfs(index + 1, leftCount, rightCount, leftRem - 1, rightRem, currentPath);
// Option 2: Keep '('
currentPath.append('(');
dfs(index + 1, leftCount + 1, rightCount, leftRem, rightRem, currentPath);
currentPath.setLength(pathLength); // Backtrack
} else if (currentChar == ')') {
// Option 1: Remove ')'
dfs(index + 1, leftCount, rightCount, leftRem, rightRem - 1, currentPath);
// Option 2: Keep ')' - only if it maintains validity (rightCount < leftCount)
// This check is implicitly handled by the pruning `rightCount > leftCount` above,
// but explicitly checking here can also work: if (leftCount > rightCount) { ... }
// We rely on the pruning check at the start of the function.
currentPath.append(')');
dfs(index + 1, leftCount, rightCount + 1, leftRem, rightRem, currentPath);
currentPath.setLength(pathLength); // Backtrack
} else {
// If it's a letter, just keep it
currentPath.append(currentChar);
dfs(index + 1, leftCount, rightCount, leftRem, rightRem, currentPath);
currentPath.setLength(pathLength); // Backtrack
}
}
}
回溯机制详解
为什么需要回溯:
- 路径共享问题:
StringBuilder
在递归过程中被多个分支共享 - 状态恢复需求:每次递归尝试后需要恢复原始状态才能进行其他尝试
具体实现:
// 以处理 '(' 为例:
currentPath.append('('); // 选择保留该括号
dfs(...); // 继续探索
currentPath.setLength(pathLength); // 回溯:撤销选择
三步回溯过程:
- 记录当前路径长度
pathLength = currentPath.length()
- 进行添加字符后的递归探索
- 通过
setLength()
回退到之前的状态
剪枝优化
- 剩余删除数校验:
if (leftRem < 0 || rightRem < 0) return;
- 有效性提前判断:
if (rightCount > leftCount) return;
- 最小删除数限制:通过预计算确定必须删除的最小括号数
两种解法对比
维度 | BFS | DFS |
---|---|---|
时间复杂度 | O(n × 2ⁿ) | O(2ⁿ) |
空间复杂度 | O(2ⁿ) | O(n) |
结果顺序 | 层级有序 | 随机顺序 |
适用场景 | 需要最短路径 | 内存敏感场景 |
复杂度分析
BFS 复杂度
- 时间复杂度:O(n × 2ⁿ)
- 最坏情况需要遍历所有可能的删除组合
- 每层处理需要 O(n) 时间验证有效性
- 空间复杂度:O(2ⁿ)
- 队列最多存储 C(n,k) 个元素(k 为最小删除数)
DFS 复杂度
- 时间复杂度:O(2ⁿ)
- 但通过剪枝优化实际远小于理论值
- 预计算最小删除数显著减少搜索空间
- 空间复杂度:O(n)
- 递归栈深度最大为 n
- 临时 StringBuilder 的空间消耗
测试用例解析
// 示例 1:
输入: "()())()"
输出: ["()()()", "(())()"]
// DFS处理流程:
1. 预计算需删除 1 ')'
2. 递归树中优先尝试删除第3个')'和第5个')'
3. 通过回溯保留有效组合
常见问题解答
Q:为什么 DFS 需要预计算最小删除数? A:这是关键剪枝策略:
- 通过遍历字符串统计必须删除的括号数
- 在 DFS 过程中严格跟踪剩余可删除数
- 避免无效路径的探索(如删除超过必要数量的括号)
Q:如何处理重复结果?
// BFS 方案:
使用 HashSet 存储已访问字符串
// DFS 方案:
自然避免重复,因为:
1. 总是按顺序处理字符
2. 删除选择具有确定性
3. 结果存储在Set中自动去重
算法选择建议
- 优先 BFS:当需要保证找到所有最短路径解时
- 选择 DFS:
- 输入字符串较长时(内存敏感)
- 需要更优的最坏情况时间复杂度
- 结合记忆化搜索可进一步优化
扩展思考
- 如何修改算法处理其他括号类型(如 {} [])?
- 如果要求返回字典序最小的解,如何优化?
- 如何实现并行化处理加速计算?
深度优化技巧
记忆化搜索改进
// 在DFS中加入记忆缓存
private Map<String, Boolean> memo = new HashMap<>();
// 修改递归入口:
if (memo.containsKey(currentPath.toString())) {
return;
}
memo.put(currentPath.toString(), true);
效果:避免重复处理相同路径,适用于含大量重复字符的场景
早期终止策略
// 在DFS开始时加入长度判断
int maxPossibleLength = len - (minLeftToRemove + minRightToRemove);
if (currentPath.length() + (len - index) < maxPossibleLength) {
return;
}