39. 组合总和
思路
回溯的时候不需要从下一个数开始了,从当前数开始即可。
思路代码
func combinationSum(candidates []int, target int) [][]int {
res:=[][]int{}
path:=[]int{}
sum:=0
var backtrack func(startindex int)
backtrack = func(startindex int){
if sum>=target{
if sum==target{
temp:=make([]int,len(path))
copy(temp,path)
res=append(res,temp)
}
return
}
for i:=startindex;i<len(candidates);i++{
path=append(path,candidates[i])
sum+=candidates[i]
backtrack(i)
path=path[:len(path)-1]
sum-=candidates[i]
}
}
backtrack(0)
return res
}
困难
注意并不是
for i:=startindex;i<len(candidates);i++{
path=append(path,candidates[i])
sum+=candidates[i]
backtrack(i)
backtrack(i+1)
path=path[:len(path)-1]
sum-=candidates[i]
}
不需要backtrack(i+1),本身在for循环中就已经满足了递归所有组合的语义了,再多一个backtrack(i+1)就会导致重复。
40.组合总和II
思路
发现要进行去重,想的是map去重,不可行
需要对同一树层上的去重,而同一树枝不用去重,使用used数组。
官方题解
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。
那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。
所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
强调一下,树层去重的话,需要对数组排序!
单层搜索的逻辑
这里与39.组合总和 (opens new window)最大的不同就是要去重了。
前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
此时for循环里就应该做continue的操作。
代码
func combinationSum2(candidates []int, target int) [][]int {
used:=make([]bool,len(candidates))
res:=[][]int{}
path:=[]int{}
sum:=0
sort.Ints(candidates)
var backtrack func(startindex int)
backtrack = func(startindex int){
if sum>=target{
if sum==target{
temp:=make([]int,len(path))
copy(temp,path)
res=append(res,temp)
}
return
}
for i:=startindex;i<len(candidates);i++{
if sum+candidates[i]>target{
break
}
if i>0&&candidates[i]==candidates[i-1]&&used[i-1]==false{
continue
}
path=append(path,candidates[i])
sum+=candidates[i]
used[i]=true
backtrack(i+1)
path=path[:len(path)-1]
sum-=candidates[i]
used[i]=false
}
}
backtrack(0)
return res
}
困难
排序为了剪枝和去重
used数组为了区分是同一树层还是同一数枝
131.分割回文串
131.分割回文串
思路
确定从哪里开始(子串的开始位置),然后回溯for循环确定切割位置;
终止条件是开始位置变成了len(s),也就是到了字符串的末尾。
思路代码
1.确定从哪里开始(子串的开始位置),然后回溯for循环确定切割位置
func partition(s string) [][]string {
res:=[][]string{}
path:=[]string{}
var backtrack func(startindex int)
backtrack = func(startindex int){
if startindex==len(s){
temp:=make([]string,len(path))
copy(temp,path)
res=append(res,temp)
return
}
for i:=startindex;i<len(s);i++{
sl:=s[startindex:i+1]
if check(sl){
path=append(path,sl)
backtrack(i+1)
path=path[:len(path)-1]
}
}
}
backtrack(0)
return res
}
func check(s string)bool{
l,r:=0,len(s)-1
for l<r{
if s[l]!=s[r]{
return false
}
l++
r--
}
return true
}
2.startindex表示开始切割的位置,i表示第二次切割的位置,语义虽然有很大变化,但代码改动不大,仅仅是for循环i的加1还是不加的区别
func partition(s string) [][]string {
res:=[][]string{}
path:=[]string{}
var backtrack func(startindex int)
backtrack = func(startindex int){
if startindex==len(s){
temp:=make([]string,len(path))
copy(temp,path)
res=append(res,temp)
return
}
for i:=startindex+1;i<=len(s);i++{
if i>0{
sl:=s[startindex:i]
if check(sl){
path=append(path,sl)
backtrack(i)
path=path[:len(path)-1]
}
}
}
}
backtrack(0)
return res
}
func check(s string)bool{
l,r:=0,len(s)-1
for l<r{
if s[l]!=s[r]{
return false
}
l++
r--
}
return true
}
官方题解
本题这涉及到两个关键问题:
切割问题,有不同的切割方式
判断回文
相信这里不同的切割方式可以搞懵很多同学了。
这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。
一些同学可能想不清楚 回溯究竟是如何切割字符串呢?
我们来分析一下切割,其实切割问题类似组合问题。
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
感受出来了不?
所以切割问题,也可以抽象为一棵树形结构,如图:
131.分割回文串
递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。
此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。
代码
var (
path []string // 放已经回文的子串
res [][]string
)
func partition(s string) [][]string {
path, res = make([]string, 0), make([][]string, 0)
dfs(s, 0)
return res
}
func dfs(s string, start int) {
if start == len(s) { // 如果起始位置等于s的大小,说明已经找到了一组分割方案了
tmp := make([]string, len(path))
copy(tmp, path)
res = append(res, tmp)
return
}
for i := start; i < len(s); i++ {
str := s[start : i+1]
if isPalindrome(str) { // 是回文子串
path = append(path, str)
dfs(s, i+1) // 寻找i+1为起始位置的子串
path = path[:len(path)-1] // 回溯过程,弹出本次已经填在的子串
}
}
}
func isPalindrome(s string) bool {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
if s[i] != s[j] {
return false
}
}
return true
}
困难
思考清楚startindex和切割位置的语义,一个是判断子串范围,一个是切割位置,二者必须有一致性(类似循环不变量)。
今日收获
回溯使用used数组去重同一树层。
回溯算法使用场景组合,分割。