目录
目录
这周主要是刷了回溯算法的题目,感觉有那么点感觉了,有点浅显的做题体会了,就给记录一下,先整体把做的题记录一下,最后做个总结,遇到回溯的算法题应该怎么写的问题。
1.组合
链接:力扣
思路:这道题明显是一个组合问题,就是说比如[1,2]和[2,1]算是一个,这种问题一般就用回溯算法,回溯算法整体思想可以给他想成遍历n叉树,其中树的深度由递归遍历深度决定,书的宽度由给的遍历范围决定。
代码:
//最终结果
var res [][]int
func combine(n int, k int) [][]int {
res=[][]int{}
backtrack(n, k, 1, []int{})
return res
}
//回溯算法
func backtrack(n,k,start int,track []int){
//满足条件后,加到最终结果上
if len(track) == k {
temp := make([]int,k)
copy(temp,track)
res = append(res,temp)
return
}
//遍历兄弟节点
for i:=start;i<=n - (k - len(track))+ 1;i++ {
track = append(track,i)
//fmt.Println(len(track))
//下一层的遍历中,需要从i+1开始选择元素,这个是核心,这个想明白了,算法就懂了
backtrack(n,k,i+1,track)
track = track[:len(track)-1]
//fmt.Println(len(track)," 20")
}
}
2.组合总和III
链接:力扣
思路:这道题和上一题特别像,就是遍历范围变为0-9,并且多添加了一个条件就是相加之和为n。
代码:
//回溯算法特别像n叉树的遍历
var res [][]int = make([][]int,0)
func combinationSum3(k int, n int) [][]int {
res = make([][]int,0)
backtrack(k,n,1,[]int{})
return res
}
func backtrack(k,n,start int,path []int) {
//满足条件则放到结果中
if len(path) == k && n == 0 {
temp := make([]int,k)
copy(temp,path)
res = append(res,temp)
return
}
//如果n《0或者长度大于k,就没有再往下层遍历(递归)的意义了
if n < 0 || len(path)> k{
return
}
for i:=start;i<=9;i++ {
path = append(path,i)
backtrack(k,n-i,i+1,path)
path = path[:len(path)-1]
}
}
3.电话号码的字母组合
链接:力扣
思路:这道题乍一看,好像和上面两个不太一样,其实核心思想是一样的,只不过稍有不同,这道题是从给定的多个集合中分别选择,上两道题只是给定了一个集合去找满足条件的组合,这道题给了数字长度个集合,让我们选择组合,这是for循环要从0开始遍历了。
代码:
var res []string
var keyMap map[int]string
func letterCombinations(digits string) []string {
if len(digits) == 0 || len(digits)>4 {
return nil
}
res = make([]string,0)
keyMap = make(map[int]string)
keyMap = map[int]string{
0:"", // 0
1:"", // 1
2:"abc", // 2
3:"def", // 3
4:"ghi", // 4
5:"jkl", // 5
6:"mno", // 6
7:"pqrs", // 7
8:"tuv", // 8
9:"wxyz", // 9
}
backTrace(digits,"",0)
return res
}
func backTrace(digits string,path string,start int) {
if len(path) == len(digits) {
res = append(res,path)
return
}
//首先确定备选集合是哪个
letters := keyMap[int(digits[start])-48]
//因为从不同集合中选择,所以要从0开始遍历
for j:=0;j<len(letters);j++ {
path += string(letters[j])
//往里走一层,就代表备选项变了
backTrace(digits,path,start+1)
path = path[:len(path)-1]
}
}
4.组合总和
链接:力扣
思路:这道题主要要注意下start从哪里开始,注意有些人认为可以重复选择元素可能就是从0开始了,这样的话就肯定是会出现重复的,是组合的重复,就是类似于【1,2】和【2,1】这种。所以start要从当前元素的索引开始。就是当前这一层选择完这个元素之后,下一层还是可以继续从这一层开始。
代码:
//和上一道题稍微有点不同,这次是拿完之后可以放回的
//什么时候有start,什么时候没有start要记清楚
//这个只要组合,不是排列,而且是在一个备选里面选择
var res [][]int
func combinationSum(candidates []int, target int) [][]int {
res = make([][]int,0)
traceBack(candidates,[]int{},target,0)
return res
}
func traceBack(candidates []int,path []int,target int,start int) {
if target == 0 {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
return
}
if target < 0 {
return
}
for i:=start;i<len(candidates);i++ {
path = append(path,candidates[i])
//这里的start要从当前元素开始
traceBack(candidates,path,target-candidates[i],i)
path = path[:len(path)-1]
}
}
5.组合总和II
链接:力扣
思路:这道题和上一道题特别类似,主要是给的条件不同了,这个题是集合中有重复元素,但是不能重复选择集合中的元素,这就要涉及到去重的问题了,两种去重方法,一种用一个used数组记录下那个用过哪个没用过,一种排序之后去重。
好好理解下注释中的这两句话
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
代码:
利用used数组去重
func combinationSum2(candidates []int, target int) [][]int {
var trcak []int
var res [][]int
var history map[int]bool
history=make(map[int]bool)
sort.Ints(candidates)
backtracking(0,0,target,candidates,trcak,&res,history)
return res
}
func backtracking(startIndex,sum,target int,candidates,trcak []int,res *[][]int,history map[int]bool){
//终止条件
if sum==target{
tmp:=make([]int,len(trcak))
copy(tmp,trcak)//拷贝
*res=append(*res,tmp)//放入结果集
return
}
if sum>target{return}
//回溯
//下面这段好要进行深刻的理解
//和这道题就行对比,一个是经过排序的一个是未经过排序的
//https://leetcode.cn/problems/increasing-subsequences/
// used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
for i:=startIndex;i<len(candidates);i++{
if i>0&&candidates[i]==candidates[i-1]&&history[i-1]==false{
continue
}
//更新路径集合和sum
trcak=append(trcak,candidates[i])
sum+=candidates[i]
history[i]=true
//递归
backtracking(i+1,sum,target,candidates,trcak,res,history)
//回溯
trcak=trcak[:len(trcak)-1]
sum-=candidates[i]
history[i]=false
}
}
利用start去重
var res [][]int
func combinationSum2(candidates []int, target int) [][]int {
res = make([][]int,0)
sort.Ints(candidates)
TraceBack(candidates,target,[]int{},0)
return res
}
func TraceBack(candidates []int, target int,path []int,start int) {
if target == 0 {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
return
}
if target < 0 {
return
}
for i:=start;i<len(candidates);i++ {
//i>start 就代表了不是当前层的开始了,而是当前层的后面的元素
if i>start && candidates[i] == candidates[i-1] {
continue
}
path = append(path,candidates[i])
TraceBack(candidates,target-candidates[i],path,i+1)
path = path[:len(path)-1]
}
}
6.分割回文串
链接:力扣
思路:这道题感觉还是挺奇特的,如果不是出现在回溯算法这块,可能还真想不到用回溯算法。用了回溯算法感觉也不知道怎么往算法上,感觉配上这个图能想的更明白一些。这道题我自己感觉也讲不太明白,多刷几次细细体会吧。
代码:
var res [][]string
func partition(s string) [][]string {
res = make([][]string,0)
TraceBack(s,0,[]string{})
return res
}
func TraceBack(s string,start int,path []string) {
if len(s) == 0{
temp := make([]string,len(path))
copy(temp,path)
res = append(res,temp)
return
}
for i:=start;i<len(s);i++ {
//这块是处理当前切割的这个子串是不是回文数
//如果不是的话就不进行添加,也不会进行到下一层
if !isHuiwen(s[start:i+1]) {
continue
}
path = append(path,s[start:i+1])
TraceBack(s[i+1:],0,path)
path = path[:len(path)-1]
}
}
func isHuiwen(s string) bool{
i,j := 0,len(s)-1
for i < j {
if s[i] != s[j] {
return false
}
i++
j--
}
return true
}
7.复原IP地址
链接:力扣
思路:这道题就是每次从s中截取一段数字字符,组成ip地址的一小段,所以我们要在循环过程中检查字串是否满足题目的要求,如果满足就添加到path中,不满足就continue
代码:
var res []string
func restoreIpAddresses(s string) []string {
res = make([]string,0)
if len(s) > 12 {
return res
}
TraceBack(s,[]string{})
return res
}
func TraceBack(s string,path []string) {
if len(s) == 0 && len(path) == 4 {
temp := strings.Join(path,".")
res = append(res,temp)
return
}
if len(path) > 4 {
return
}
for i:=0;i<len(s)&&i<3;i++ {
if !isHeGe(s[:i+1]) {
continue
}
path = append(path,s[:i+1])
TraceBack(s[i+1:],path)
path = path[:len(path)-1]
}
}
func isHeGe(s string) bool{
if s[0] == '0' && len(s)>1{
return false
}
temp,_ := strconv.Atoi(s)
if temp > 255 {
return false
}
return true
}
8.子集
链接:力扣
思路:这道题就是不需要判断条件,只要是结果组合,直接加进去就行了
代码:
//这道题就是完全没有限制的一个组合问题,这个就好像是收集所有的叶子节点
var res [][]int
func subsets(nums []int) [][]int {
res = make([][]int,0)
TraceBack(nums,[]int{},0)
return res
}
func TraceBack(nums []int,path []int,start int) {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
for i:=start;i<len(nums);i++ {
path = append(path,nums[i])
TraceBack(nums,path,i+1)
path = path[:len(path)-1]
}
}
9.子集II
链接:力扣
思路:这道题只比上一道题多了一个去重的,上面讲过两个去重方法
代码:
var res [][]int
func subsetsWithDup(nums []int) [][]int {
res = make([][]int,0)
sort.Ints(nums)
TraceBack(nums,[]int{},0)
return res
}
func TraceBack(nums []int,path []int,start int) {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
for i:=start;i<len(nums);i++ {
//这块是为了去重而准备的,不能在同一层上重复选择一个数值
//大概意思就是,在树的这一层上,这个数值出现的第一次可以选,第二次就不能再选了,否则就重复
if i>start && nums[i] == nums[i-1] {
continue
}
path = append(path,nums[i])
TraceBack(nums,path,i+1)
path = path[:len(path)-1]
}
}
10.递增子序列(11111)
链接:力扣
思路:这道题,有点难度,这道题让选择递增子序列,就是不允许你给原来的序列进行排序,只能在子序列上进行操作。这就不能用start那种方式去重了,只能用used数组去充了。而且这个去重是在层级上进行去重。
代码:
var res [][]int
func findSubsequences(nums []int) [][]int {
res = make([][]int,0)
TraceBack(nums,[]int{},0)
return res
}
func TraceBack(nums []int,path []int,start int) {
if len(path) > 1 {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
}
//每深一层递归,都会有一个新的used数组用来记录本层的元素是否使用过
//相同元素每一层只能使用一次,使用第二次的话就会有重复元素
var used map[int]bool = make(map[int]bool)
for i:=start;i<len(nums);i++ {
//这个是为了去除重复元素的
//现在nums里面是无序的,所以只能用used数组来记录本层的元素是否重复出现过
if i>start && used[nums[i]] == true {
continue
}
//这个是为了满足递增条件的
if len(path) > 0 && nums[i] < path[len(path)-1] {
continue
}
path = append(path,nums[i])
used[nums[i]] = true
TraceBack(nums,path,i+1)
path = path[:len(path)-1]
}
}
11.全排列
链接:力扣
思路:这道题就是排列问题了,需要在树枝上去重,好好体会下和树层上去重有什么区别。
代码:
var res [][]int
var used map[int]bool
func permute(nums []int) [][]int {
res = make([][]int,0)
used = make(map[int]bool)
TraceBack(nums,[]int{})
return res
}
func TraceBack(nums []int,path []int,) {
if len(path) == len(nums) {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
return
}
for i:=0;i<len(nums);i++ {
if used[nums[i]] {
continue
}
path = append(path,nums[i])
used[nums[i]] = true
TraceBack(nums,path)
path = path[:len(path)-1]
used[nums[i]] = false
}
}
12.全排列 II(11111111)
链接:力扣
思路:这道题是排列问题,这道题听复杂的,需要在树枝和层级上同事去重
还需要注意去重数字used的变量类型,因为这道题有重复元素,所以不能使用map类型来充当去重元素,只能用int类型。
代码:
//需要树枝和层级上的双重去重
var res [][]int
var used []int
func permuteUnique(nums []int) [][]int {
res = make([][]int,0)
used = make([]int,len(nums))
sort.Ints(nums)
TraceBack(nums,[]int{})
return res
}
func TraceBack(nums []int,path []int) {
if len(path) == len(nums) {
temp := make([]int,len(path))
copy(temp,path)
res = append(res,temp)
return
}
for i:=0;i<len(nums);i++ {
//在树枝上去重,如果这个数字已经用过了,则不能在选择他了
if used[i] == 1 {
continue
}
//层级上去重
//注意used[i-1] == 0,就是说有一个相等的,并且还没进行选择
//就是说之前的遍历已经有从这个数字开头的的了,所以就不能在以这个数字开头了
if i>0 && nums[i] == nums[i-1] && used[i-1] == 0{
continue
}
path = append(path,nums[i])
used[i] = 1
TraceBack(nums,path)
path = path[:len(path)-1]
used[i] = 0
}
}
总结,回溯算法这里,总体上分为两大类,就是组合问题和排列问题。组合一般start使用i+1开始的,而排列是从0开始的。还有就是去重方式上的差别,到底应该层级上去重还是树枝上去重。
怎样有效进行去重:全局变量used,就是代表从树枝上去重
回溯函数内的变量,就是从层级上去重
回溯的代码模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}