https://leetcode-cn.com/problems/remove-invalid-parentheses/
这题要求所有的解,所以我们应当想到用搜索来解决,BFS的思想会比较直观:
注意: 容易发现,每个level会有很多重复的字符串,所以我们需要用集合来进行去重。
如何判断一个字符串合法?可以通过左右括号的数量来判断。遍历过程中右括号的数量必须小于等于左括号的数量,最终左右括号数量必须相等,这才是一个合法的字符串。
class Solution {
public List<String> removeInvalidParentheses(String s) {
List<String> ans = new LinkedList<String>();
Queue<String> level = new LinkedList<String>();
Set<String> visited = new HashSet<String>();
level.offer(s);
while (!level.isEmpty() && ans.isEmpty()) {
int size = level.size();
visited.clear();
for (int i = 0; i < size; ++i) {
s = level.poll();
if (isValid(s)) {
ans.add(s);
continue;
}
for (int k = 0; k < s.length(); ++k) {
if (s.charAt(k) == '(' || s.charAt(k) == ')') {
String str = s.substring(0, k) + s.substring(k + 1);
if (!visited.contains(str)) {
visited.add(str);
level.offer(str);
}
}
}
}
}
return ans;
}
private boolean isValid(String s) {
int left = 0, right = 0;
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c == '(') left++;
else if (c == ')') right++;
if (left < right) return false;
}
return left == right;
}
}
BFS的解法虽然简单,但是时间复杂度却比较高,因为我们对一个level中的所有字符串都进行了一遍遍历来判断是否合法,生成可能的字符串的过程也不够优雅。
DFS(回溯)的想法十分巧妙,它不以删除括号为目标,而是通过递归去遍历原始字符串,在递归过程中维护一个expression,最终的expression就是一个可能的解,需要判断其是否合法。 递归过程的细节:
rightCount <= leftCount
的expression才可能是一个合法的字符串,如果最终leftCount == rightCount
,则expression是一个合法的字符串class Solution {
private Set<String> ans = new HashSet<String>();
private int minRemoved = Integer.MAX_VALUE;
public List<String> removeInvalidParentheses(String s) {
backtrack(s, 0, 0, 0, new StringBuilder(), 0);
return new ArrayList<String>(this.ans);
}
private void backtrack(String s, int index, int leftCount, int rightCount, StringBuilder expression, int removed) {
if (index == s.length()) {
if (leftCount == rightCount && removed <= minRemoved) {
if (removed < minRemoved) {
ans.clear();
this.minRemoved = removed;
}
this.ans.add(expression.toString());
}
}
else {
char c = s.charAt(index);
int length = expression.length();
if (c != '(' && c != ')') {
expression.append(c);
backtrack(s, index + 1, leftCount, rightCount, expression, removed);
expression.deleteCharAt(length);
}
// 注意这个else里会前后执行两次backtrack,
// 前一次backtrack对expression造成的修改不应该影响第二次backtrack
// 所以需要回溯,删去对expression的修改
else {
// 删掉这个括号
backtrack(s, index + 1, leftCount, rightCount, expression, removed + 1);
// 不删掉,即加入expression
expression.append(c);
if (c == '(') backtrack(s, index + 1, leftCount + 1, rightCount, expression, removed);
else if (rightCount < leftCount) backtrack(s, index + 1, leftCount, rightCount + 1, expression, removed);
expression.deleteCharAt(length);
}
}
}
}
解法二的思想虽然巧妙地减少了判断字符串是否合法的时间,但由于会生成非常多的expression,时间复杂度同样较高(BFS则不会生成太多,若在某一个level找到解,则停止进入下一个level)。
为什么解法二会生成非常多的expression?因为我们没有对删除的括号数量进行限制,而是先删除,再比较删除数量的大小,这样自然就会导致很多生成的expression其对应的删除数量很大,并不是我们想要的解。简单来说,就是没有充分剪枝。
如果我们能从原始字符串中找出需要删除的左右括号数量leftRem
和rightRem
,那么在回溯过程中就能通过限制删除括号的数量,进行很多剪枝,大量地减少递归的次数。
我们可以通过一次遍历来找到leftRem
和rightRem
:
c == '('
,则leftRem++
c == ')'
,则需要判断它是否有对应的(
,如果有,则是合法的,如果没有,则是非法的。具体地来说,如果leftRem == 0
,则rightRem++
;否则leftRem--
leftRem
和rightRem
就是我们想要的结果class Solution {
private Set<String> ans = new HashSet<String>();
public List<String> removeInvalidParentheses(String s) {
// 统计应该删除多少左括号和右括号
int leftRem = 0, rightRem = 0;
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c == '(') leftRem++;
else if (c == ')') {
if (leftRem == 0) rightRem++;
else leftRem--;
}
}
backtrack(s, 0, 0, 0, leftRem, rightRem, new StringBuilder());
return new ArrayList(this.ans);
}
private void backtrack(String s, int index, int leftCount, int rightCount, int leftRem, int rightRem, StringBuilder expression) {
if (index == s.length()) {
if (leftCount == rightCount) this.ans.add(expression.toString());
}
else {
char c = s.charAt(index);
int length = expression.length();
if (c != '(' && c != ')') {
expression.append(c);
backtrack(s, index + 1, leftCount, rightCount, leftRem, rightRem, expression);
expression.deleteCharAt(length);
}
// 注意这个else里会前后执行两次backtrack,
// 前一次backtrack对expression造成的修改不应该影响第二次backtrack
// 所以需要回溯,删去对expression的修改
else {
// 删掉这个括号
if (c == '(' && leftRem > 0) backtrack(s, index + 1, leftCount, rightCount, leftRem - 1, rightRem, expression);
else if (rightRem > 0) backtrack(s, index + 1, leftCount, rightCount, leftRem, rightRem - 1, expression);
// 不删掉,即加入expression
expression.append(c);
if (c == '(') backtrack(s, index + 1, leftCount + 1, rightCount, leftRem, rightRem, expression);
else if (rightCount < leftCount) backtrack(s, index + 1, leftCount, rightCount + 1, leftRem, rightRem, expression);
expression.deleteCharAt(length);
}
}
}
}