这一题没有特别特别取巧的地方,两种方法去做。dfs和bfs,毕竟这是一道需要遍历所有情况的题目,目前看来没有更取巧的方法了。
如果采用dfs的方式,是需要预处理的。当然,dfs的本质就是遍历字符串,然后根据是否取当前字符分开两种情况进行dfs。但如果只是单纯的这样做性能会非常糟糕,因为会把所有valid的string都放进结果集而并不是特定长度的。所以我们需要预处理这个字符串,知道最长的valid string应该有多长,删掉多少左框或者右框。一开始我以为需要参考https://blog.csdn.net/chaochen1407/article/details/43230047 但发现我的理解是错的。那一题要找的是substring,这一题是随意删除括弧。所以需要的是维护两个变量,当前所需要删除的左括弧leftPar,当前所需要删除的右括弧rightPar。自左向右,遇到左括弧leftPar加一,遇到右括弧的时候如果leftPar大于0那么leftPar就减一,否则rightPar加一。这个和longest valid parentheses不一样,不需要两方向扫两次,没有意义。所以走到某个位置的时候,leftPar和rightPar的值就表示需要删除多少个左括弧和有括弧能得到一个最长的parentheses。举个例子,(())()())(( 这个字符串里,leftPar = 2, rightPar = 1,所以只需要删除两个左括弧和一个有括弧就能变成最长的parentheses(())()()了。
得到了leftPar和rightPar,我们就可以在原来的dfs基础上加停下来的机制。当我们在dfs的时候,删掉一个左括弧leftPar就减一,删掉一个有括弧rightPar就减一。如果leftPar或者rightPar等于0我们就不再走对应的删除括弧的dfs分枝。当字符已经是一个合理的parentheses而且leftPar和rightPar等于0的时候,我们就得到了一个答案了。在dfs里,比较容易去除duplicate答案的方式应该还是HashSet吧。给出代码如下:
public List removeInvalidParentheses(String s) {
return this._dfsResolve(s);
}
private List _dfsResolve(String s) {
int leftToRem = 0, rightToRem = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '(') {
leftToRem++;
} else if (ch == ')') {
if (leftToRem > 0) {
leftToRem--;
} else {
rightToRem++;
}
}
}
Set resultSet = new HashSet<>();
_doDfs(resultSet, new StringBuilder(), s, leftToRem, rightToRem, 0, 0);
return new LinkedList(resultSet);
}
private void _doDfs(Set resultSet, StringBuilder sb, String s, int leftToRem, int rightToRem, int open, int curPos) {
if (curPos == s.length() && leftToRem == 0 && rightToRem == 0 && open == 0) {
resultSet.add(sb.toString());
}
if (leftToRem < 0 || rightToRem < 0 || open < 0 || curPos >= s.length()) {
return;
}
int curLen = sb.length();
char ch = s.charAt(curPos);
if (ch == '(') {
_doDfs(resultSet, sb, s, leftToRem - 1, rightToRem, open, curPos + 1);
_doDfs(resultSet, sb.append(ch), s, leftToRem, rightToRem, open + 1, curPos + 1);
} else if (ch == ')') {
_doDfs(resultSet, sb, s, leftToRem, rightToRem - 1, open, curPos + 1);
_doDfs(resultSet, sb.append(ch), s, leftToRem, rightToRem, open - 1, curPos + 1);
} else {
_doDfs(resultSet, sb.append(ch), s, leftToRem, rightToRem, open, curPos + 1);
}
sb.setLength(curLen);
}
这里有一点要注意的是关于StringBuilder的应用。它通过setLength来实际删除append的最后一个字母。然后用的是set去重复再重构一个list出来。
第二种办法就是bfs。bfs的原理其实不复杂,就是从头到尾遍历删除括号然后整理出不同的情况push到queue里面。然后遇到第一个valid的组合之后就不再push到queue直到把queue里面剩下的全部字符串全部验证完毕即可。
举例,在字符串(a(b())里,第一个遍历就会产生a(b()),(ab()),(a(b)), (a(b(), (a(b()。因为实际上(a(b()会出现多次,需要用到一个set来记录自己走过的情况。同时,连续的同样的括号也可以略过,作为去重复的环节我们可以认为我们同一层bfs都只删除连续括号中的第一个(这个思路接下来还可以简化dfs的做法)。当遇到第一个符合条件的子字符串的时候你就确定了答案的长度,然后你就不需要再分裂检查和push任何进queue里了。当queue为空或者push出第一个比结果长度更短的字符串的时候你就知道已经结束了,因为你已经知道结果长度了,queue里面剩下的都是更短的长度了。
根据上述描述,给出代码如下:
public List removeInvalidParentheses(String s) {
return this._bfsResolve(s);
}
private List _bfsResolve(String s) {
Queue bfsQ = new LinkedList<>();
Set visited = new HashSet<>();
List result = new LinkedList<>();
int resLen = -1;
visited.add(s);
bfsQ.add(s);
while (!bfsQ.isEmpty()) {
String curS = bfsQ.poll();
if (resLen != -1 && curS.length() < resLen) break;
if (isValid(curS)) {
resLen = curS.length();
result.add(curS);
}
for (int i = 0; i < curS.length(); i++) {
if (i > 0 && curS.charAt(i) == curS.charAt(i - 1)) continue;
String nextS = curS.substring(0, i) + curS.substring(i + 1, curS.length());
if (visited.contains(nextS)) continue;
visited.add(nextS);
bfsQ.add(nextS);
}
}
return result;
}
public boolean isValid(String s) {
int open = 0;
for (int i = 0; i < s.length() && open >= 0; i++) {
char ch = s.charAt(i);
if (ch == '(') {
open++;
} else if (ch == ')') {
open--;
}
}
return open == 0;
}
根据上面说的,去重复而且不利用hashset的方式有一种,就是连续的同一种括号,只删掉第一个作为分支,否则跳过。这样的去重复也可以应用在dfs上,这样dfs的解就不需要使用到HashSet。
public List removeInvalidParentheses(String s) {
return this._dfsResolve(s);
}
private List _dfsResolve(String s) {
int leftToRem = 0, rightToRem = 0;
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
if (ch == '(') {
leftToRem++;
} else if (ch == ')') {
if (leftToRem > 0) {
leftToRem--;
} else {
rightToRem++;
}
}
}
List result = new LinkedList<>();
_doDfs(result, new StringBuilder(), s, leftToRem, rightToRem, 0, 0);
return result;
}
private void _doDfs(List result, StringBuilder sb, String s, int leftToRem, int rightToRem, int open, int curPos) {
if (curPos == s.length() && leftToRem == 0 && rightToRem == 0 && open == 0) {
result.add(sb.toString());
}
if (leftToRem < 0 || rightToRem < 0 || open < 0 || curPos >= s.length()) {
return;
}
int curLen = sb.length();
char ch = s.charAt(curPos);
boolean skip = curLen > 0 && sb.charAt(curLen - 1) == ch;
if (ch == '(') {
if(!skip) _doDfs(result, sb, s, leftToRem - 1, rightToRem, open, curPos + 1);
_doDfs(result, sb.append(ch), s, leftToRem, rightToRem, open + 1, curPos + 1);
} else if (ch == ')') {
if(!skip) _doDfs(result, sb, s, leftToRem, rightToRem - 1, open, curPos + 1);
_doDfs(result, sb.append(ch), s, leftToRem, rightToRem, open - 1, curPos + 1);
} else {
_doDfs(result, sb.append(ch), s, leftToRem, rightToRem, open, curPos + 1);
}
sb.setLength(curLen);
}
和最上面的代码对比起来,你可以发现我只是把HashSet改成了直接的List,另外,加了一个skip判定,就判定当前的括号是否一连串的连续相同的括号中的第一个,不是的话就不产生删除的分支。这样可以提高大约三倍的效率也不需要重新从set重构回list。