22年43周

目录

目录

1.组合

2.组合总和III

3.电话号码的字母组合

4.组合总和

5.组合总和II

6.分割回文串

7.复原IP地址

8.子集

9.子集II

10.递增子序列(11111)

11.全排列

12.全排列 II(11111111)



这周主要是刷了回溯算法的题目,感觉有那么点感觉了,有点浅显的做题体会了,就给记录一下,先整体把做的题记录一下,最后做个总结,遇到回溯的算法题应该怎么写的问题。

1.组合

链接:力扣

思路:这道题明显是一个组合问题,就是说比如[1,2]和[2,1]算是一个,这种问题一般就用回溯算法,回溯算法整体思想可以给他想成遍历n叉树,其中树的深度由递归遍历深度决定,书的宽度由给的遍历范围决定。

77.组合

代码:

//最终结果
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(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值