思路可以看懂,但是想解决的细节,梳理思路好费脑细胞啊😵
看到题目首先脑子里回溯,动态规划想了一遍,发现没有特别有效/巧妙的解法,接着开始思考BFS,DFS,暴力求解的方法 ,也参考了网上各路大神的解法,最后发现解决的核心在于以下基本步骤,构建返回值列表 res,初始为空:
1)针对字符串 s ,依次遍历其删除 i(因为要求计算删除括号个数最小的情况,所以i的取值为 0,1,2...) 个括号的所有情况
2)当s在删除 i 个括号的某一情况下合法时,将该情况下删除括号后的s加入res中
3)遍历完删除 i 个字符的所有情况后,若res不为空,返回res;若res为空,遍历删除i+1个括号的情况
所有的题解基本都是围绕这个思路的各种优化。
1、BFS(结合队列的数据结构)
1)数据结构
- s string:题目所给的字符串
- res []string:存放返回值的数组
- visit map[string]bool:用于判断该字符串是否已经遍历过,避免重复操作
- queue []string:存放删除i个字符后的待判断字符串
- flag bool:标志当前res是否为空,true表明已找到满足要求的字符串,删除当前个数字符的字符串可以构成合法字符串,不再进行后续的删除字符操作
2)解题思路
提前构建 isValid(str string) bool 函数,用于判断字符串s是否合法
- 初始化 res=[ ],visit[s]=true,queue=[s, ]
- 当 queue 不为空时,弹出队首字符串 cur,判断 cur 是否合法:1)合法,将 cur 加入到 res中,flag 设置为 true,继续遍历直至队列为空。2)不合法 && flag 为 false,对当前字符串 cur 进行遍历,如果为括号字符的话将该字符删除得到新的字符串 str(注意:因为s中存在非括号字符,这里只删除括号),判断 str 是否已经在 visit 中,若不在则将 str 加入到 queue 的队尾。
- 遍历结束后返回结果数组 res。
注:在构建思路的过程中遇到如下差点没想通的问题,以 s="()())()"为例(结果:删除1个括号字符可满足条件,最后答案为 ["(())()","()()()"]),当遍历到i=1,cur=“)())()”时,非法且flag=false,在这种情况下,会将i=2的 “())()”,“)))()”,“)()()”,“)()))”,“)())(” 情况加入到队列中并在后续遍历队列的过程中判断字符串是否合法同时将合法字符添加到res中,但实际上i=1时存在合法字符,不应该考虑i=2的情况,如果i=2的情况下存在合法字符会造成返回错误。
同样用上述例子在脑子里跑了一遍代码后发现并不会出现错误,因为i=1的情况下存在合法字符,且队列的数据结构会保证先遍历完i=1的情况再遍历i=2的情况,那么再遍历i=2的情况前 flag一定会被设置为true(这保证了不会有i=3的情况加入到队列)。考虑到括号都是成对出现,如果i=1的情况下字符串合法,那么i=2的情况下不可能会存在合法字符串,加入到队列中的i=2的情况肯定不会被加入到res中,保证了最后结果的正确性。
func removeInvalidParentheses(s string) []string {
var res []string
var visit map[string]bool=make(map[string]bool)
var queue []string
var flag bool=false
visit[s]=true
queue=append(queue, s)
for len(queue)>0{
cur:=queue[0]
queue=queue[1:]
if isValid(cur){
res=append(res, cur)
flag=true
}
if flag { continue }
for k, v:=range cur{
if v=='(' || v==')'{
str:=cur[:k]+cur[k+1:]
if _, ok:=visit[str]; !ok{
visit[str]=true
queue=append(queue, str)
}
}
}
}
return res
}
func isValid(s string) bool{
cnt:=0
for _, c := range s{
if c=='(' { cnt++ }
if c==')' {
cnt--
if cnt<0 { return false }
}
}
if cnt==0{ return true }
return false
}
2、递归回溯
用递归的方法解题,感觉就是在暴力的基础上添加一些剪枝的操作,想到的剪枝操作包括
(1)用cntLeft 和 cntRight分别记录当前非法的左右括号数:二者一个为0,一个非0,非0的一方有多出来的括号;二者均非0,说明存在非法的括号组合,例如“)(”。当且仅当 cntLeft和cntRight均为0时,使用 isValid 判断字符串是否合法
(2)记录递归前的遍历位置,接着之前的位置开始遍历,如果每次都从头开始,会有很多重复的递归操作
(3)对于类似“())”的情况,删除第一个或是第二个“)”并没有区别,所以保证删除的当前括号与它相邻的字符不相同(这个情况也可以使用BFS中的visit数组)
根据上述思路,实现如下:
1)数据结构
- s string:题目所给的字符串
- res []string:存放返回值的数组
2)解题思路
递归函数参数:cntLeft,cntRight,i(遍历的起始位置),str(当前的字符串)
- 初始化 res=[ ];
- 根据初始的字符串 s 分别计算 cntLeft 与 cntRight。计算过程:遍历 s,当遇到'('时,cntLeft++;当遇到')'且cntLeft>0时,cntLeft--;当遇到')'且cntLfet==0时,cntRight++
- 进入递归函数:(1)当 cntLeft 与 cntRight 均为0时,调用 isValid 判断当前字符串str是否合法,合法,将str加入res中,返回(2)当 cntLeft 与 cntRight 并不均为0,从 i 开始遍历字符串 s:(a) cntLeft>0,删除遇到的非重复的 '(',调用递归函数,更新 cntLeft 为 cntLeft-1,i 更新为当前遍历到的位置;(b) cntRight>0,删除遇到的非重复的 ‘)’,调用递归函数,更新cntRight 为 cntRight-1,i 更新为当前遍历到的位置。
- 递归结束后返回结果数组 res。
注:思考这种递归的解法应该如何保证删除的是个数最少的字符?因为仅在 cntLeft 与 cntRight 不为0时进行删除操作,因为当二者均为0时,证明此时一定有某一种情况下字符串是合法的。当二者为0时,不论字符串是否合法,当前递归均会结束,不会再删除字符,保证了删除的是最小数量的字符。
func removeInvalidParentheses(s string) []string {
var res []string
var cntL, cntR int=0, 0
var helper func(cntLeft, cntRight, index int, str string)
helper=func(cntLeft, cntRight, index int, str string) {
if cntLeft==0 && cntRight==0{
if isValid(str) { res=append(res, str) }
return
}
for i:=index; i<len(str); i++{
if i==index || str[i]!=str[i-1] {
new:=str[:i]+str[i+1:]
if cntLeft>0 && str[i]=='('{ helper(cntLeft-1, cntRight, i, new) }
if cntRight>0 && str[i]==')'{ helper(cntLeft, cntRight-1, i, new) }
}
}
}
for _, v:=range s{
if v=='(' { cntL++ }
if v==')' {
if cntL>0 {
cntL--
} else { cntR++ }
}
}
helper(cntL, cntR, 0, s)
return res
}
func isValid(s string) bool{
cnt:=0
for _, c := range s{
if c=='(' { cnt++ }
if c==')' {
cnt--
if cnt<0 { return false }
}
}
if cnt==0{ return true }
return false
}
3、还看到一个很巧妙的括号翻转的做法,我懒得再编码实现了,想看的话直接见[LeetCode] 301. Remove Invalid Parentheses 移除非法括号 - Grandyang - 博客园 (cnblogs.com)