77. 组合

题目描述

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按 任何顺序 返回答案。
image.png

思路

我们可以把组合问题抽象为以下树形结构:
image.png
从上图可以看出,首先在[1 2 3 4 ]中取一个数,每一个数都要取一次,所以这里有个大的for循环。
for i := 0; i < n ;i++
比如,取了1以后,下面在第一次取1的这个情况下进行操作。

  1. for i := start;i <= n;i++{
  2. path = append(path,i)
  3. backtracing(n,k,i+1,path)
  4. path = path[:len(path)-1]
  5. }

当 i 取1以后开始对[2 3 4]进行取值,这里进行一下i+1,取到了2(注意这时候是在for i := 1; i < n ;i++)。这时候path里面有了2个数和k相等,这时候就要返回,第一次返回以后这时候path=[ 1 2 ],然后走path = path[:len(path)-1]这条代码,path = [ 1 ]。for i := 1; i < n ;i++这个循环才进行一次,下一轮i = 2,path取到了3,path = [ 1 3 ],这时候因为进行到了最后一层,所以这个循环每一次进行完以后,就会返回,path不断进行删除和添加元素操作。当这一轮的for i := 1; i < n ;i++循环走完以后。注意:每一次的添加元素和删除元素成对出现的,所以,在i取1的这个大前提下,所进行的后面每一个对path的操作都是添加删除。也即是一直在backtracing(n,k,i+1,path)这里面进行操作的。这时候backtracing(n,k,i+1,path)执行完毕后,继续执行path = path[:len(path)-1],把1从path中删除,path为空数组。下一大轮次i就取2了。如此反复。

优化

来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。
image.png
图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。
优化过程如下:

  1. 已经选择的元素个数:len(path)
  2. 还需要的元素个数为:k - len(path)
  3. 在集合n中至多要从该起始位置 : n - (k - len(path)) + 1,开始遍历,为什么有个+1呢,因为包括起始位置。

举个例子,n = 4,k = 3, 目前已经选取的元素为0,n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
所以优化之后的for循环是:
for i := start;i <= n - (k - len(path)) + 1;i++

运行代码

  1. var res [][]int
  2. func combine(n int, k int) [][]int {
  3. res = [][]int{}
  4. if n <= 0 || k <= 0 || k > n {
  5. return res
  6. }
  7. backtracing(n, k, 1, []int{})
  8. return res
  9. }
  10. func backtracing(n,k,start int,path []int){
  11. if len(path) == k{
  12. temp := make([]int,k)
  13. copy(temp,path)
  14. res = append(res,temp)
  15. return
  16. }
  17. for i := start;i <= n - (k - len(path)) + 1;i++{
  18. path = append(path,i)
  19. backtracing(n,k,i+1,path)
  20. path = path[:len(path)-1]
  21. }
  22. }

上面需要注意的是,当长度等于K的时候,不可以直接res = appen(res, path),因为res中存放的是path切片,res[ 0 ] = path,res[ 1 ] = path,res[ 2 ] = path.....这样之后每次对path修改都是对切片进行修改,同时肯定就伴随着会修改res内部元素。也即出现这样的结果
image.png只会记录最后一次对path的修改。
为了避免这种情况我们采用代码中写法,把每一时刻的path记录下来。


216.组合总和III

题目描述

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
image.png

思路

大体思路和上一题77.组合差不多。各部分解释如下:
for 循环是从1到9,k还是不变,引入一个sum。其实就只是在终止条件哪里有一点变化。

运行代码

  1. var res [][]int
  2. func combinationSum3(k int, n int) [][]int {
  3. res = [][]int{}
  4. if n <= 0 || k <= 0 || k > n{
  5. return res
  6. }
  7. backtracking(n,k,1,[]int{},0)
  8. return res
  9. }
  10. func backtracking(n,k,start int,path []int,sum int){
  11. if len(path) == k{
  12. if sum == n{
  13. temp := make([]int,k)
  14. copy(temp,path)
  15. res = append(res,temp)
  16. }
  17. return
  18. }
  19. for i:=start;i<=9-(k-len(path))+1;i++{
  20. sum += i
  21. path = append(path,i)
  22. backtracking(n,k,i+1,path,sum)
  23. sum -= i
  24. path = path[:len(path)-1]
  25. }
  26. }

17.电话号码的字母组合

题目描述

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
image.png
image.png
提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 [‘2’, ‘9’] 的一个数字。

    思路

    例如:输入:”23”,抽象为树形结构,如图所示:
    image.png
    当在第一层取一个字母的时候,第二层就是第二个数字对应的字母。

    运行代码

    1. var res []string
    2. func letterCombinations(digits string) []string {
    3. length := len(digits)
    4. if length == 0 || length > 4{
    5. return nil
    6. }
    7. digitsMap := [10]string{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"}
    8. res=make([]string,0)
    9. backtracking("" ,digits, 0,digitsMap)
    10. return res
    11. }
    12. func backtracking(tempString ,digits string, Index int,digitsMap [10]string){
    13. if len(digits) == len(tempString){
    14. res = append(res,tempString)
    15. return
    16. }
    17. tmpK:=digits[Index]-'0'
    18. letter:=digitsMap[tmpK]
    19. for i:=0;i<len(letter);i++{
    20. tempString=tempString+string(letter[i])//拼接结果
    21. backtracking(tempString,digits,Index+1,digitsMap)
    22. tempString=tempString[:len(tempString)-1]//回溯
    23. }
    24. }

    39. 组合总和

    题目描述

    给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按任意顺序返回这些组合。
    candidates 中的同一个 数字可以无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
    对于给定的输入,保证和为 target 的不同组合数少于 150 个。
    image.png

    思路

    这道题可以无限制重复选取数组元素,具体思路如下图:
    image.png
    因为所有组合不能重复,所以取完第一层数据以后,第二层并不是继续原始数组。
    这里主要是两点我自己没想好就开始写代码的。
    1、当sum > target 的时候就应该返回,不然会无限选择。
    2、对于组合问题,什么时候需要startIndex呢?
    答:如果是求的组合问题,不包含重复元素,就需要每一层起始位置不一样。
    这里startIndex应该和第一层取的元素下标一样即可,保证取的组合不重复。()

    运行代码

    1. var res [][]int
    2. func combinationSum(candidates []int, target int) [][]int {
    3. res = [][]int{}
    4. if len(candidates) == 0 || target < 0{
    5. return res
    6. }
    7. backtracing(0,target,0,candidates,[]int{})
    8. return res
    9. }
    10. func backtracing(start,target,sum int,candidates []int,path []int){
    11. if sum == target{
    12. temp := make([]int,len(path))
    13. copy(temp,path)
    14. res = append(res,temp)
    15. return
    16. }
    17. if sum > target{
    18. return
    19. }
    20. for i := start;i < len(candidates);i++{//这里起始位置一定要是start,不可以是0,因为这样才
    21. //保证组合不重复
    22. sum += candidates[i]
    23. path = append(path,candidates[i])
    24. backtracing(i,target,sum,candidates,path)
    25. sum -= candidates[i]
    26. path = path[:len(path)-1]
    27. }
    28. }

    40. 组合总和 II

    题目描述

    给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
    candidates 中的每个数字在每个组合中只能使用 一次 。
    注意:解集不能包含重复的组合。
    image.png

    思路

    一看到这个题,首先确定,求和为target,这时候应该能想到终止条件。
    因为不可以含有重复组合,所以递归的时候起始位置为i。每个元素只能使用一次,所以start为 i+1。
    元素也不可以重复用,所以就需要对数组进行排序,然后在for循环的时候,同一层不可以有相同元素。

    运行代码

    ```go

var res [][]int func combinationSum2(candidates []int, target int) [][]int { res = [][]int{} sort.Ints(candidates) if len(candidates) == 0 || target < 0{ return res } backtracing(0,target,0,candidates,[]int{}) return res } func backtracing(start,target,sum int,candidates []int,path []int){ if sum == target{ temp := make([]int,len(path)) copy(temp,path) res = append(res,temp) return } if sum > target{ return } for i := start;i < len(candidates);i++{//这里起始位置一定要是start,不可以是0,因为这样才 //保证组合不重复 if i>start && candidates[i] == candidates[i-1]{ continue }
sum += candidates[i] path = append(path,candidates[i]) backtracing(i+1,target,sum,candidates,path) sum -= candidates[i] path = path[:len(path)-1] } }

  1. <a name="G8BMZ"></a>
  2. ## [131. 分割回文串](https://leetcode.cn/problems/palindrome-partitioning/)
  3. <a name="ufk1Y"></a>
  4. ### 题目描述
  5. 给你一个字符串 s,请你将_ _s_ _分割成一些子串,使每个子串都是 **回文串** 。返回 s 所有可能的分割方案。<br />**回文串** 是正着读和反着读都一样的字符串。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25888738/1654607354346-b139cfb9-c8c1-460b-ac2e-40a6a501b7f4.png#clientId=ufff688ad-7709-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=202&id=uc9da4c4e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=253&originWidth=676&originalType=binary&ratio=1&rotation=0&showTitle=false&size=7101&status=done&style=none&taskId=u0c144e07-e65e-470d-834f-b2a9781ab42&title=&width=540.8)
  6. <a name="q3D9j"></a>
  7. ### 思路
  8. 分析一下怎么切割,只看第一层循环,第一次切割找到start:i,也就是0:0,第一个字符,然后第二次找0:2,前两个字符,这样一次类推。这是第一层。假如找到第一个字符,然后开始切割后面的,使用同样的方法,不断找当第一种切割的前提下,其他的切割方法。<br />如下图,第一次切a,下面只能切a,切ab。第二次切aa,下一次切b,第三次切aab。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25888738/1654609163618-21d45150-0b64-4461-8d00-14b4b5324558.png#clientId=ufff688ad-7709-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u1859d8e5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=840&originWidth=1456&originalType=url&ratio=1&rotation=0&showTitle=false&size=299585&status=done&style=none&taskId=u3a7842e2-50d7-4fd3-8f78-3f4bdf9dbac&title=)
  9. <a name="e57dU"></a>
  10. ### 运行代码
  11. ```go
  12. func partition(s string) [][]string {
  13. var tmpString []string//切割字符串集合
  14. var res [][]string//结果集合
  15. backTracking(s,tmpString,0,&res)
  16. return res
  17. }
  18. func backTracking(s string,tmpString []string,startIndex int,res *[][]string){
  19. if startIndex==len(s){//到达字符串末尾了
  20. //进行一次切片拷贝,怕之后的操作影响tmpString切片内的值
  21. t := make([]string, len(tmpString))
  22. copy(t, tmpString)
  23. *res=append(*res,t)
  24. }
  25. for i:=startIndex;i<len(s);i++{
  26. //处理(首先通过startIndex和i判断切割的区间,进而判断该区间的字符串是否为回文,若为回文,则加入到tmpString,否则继续后移,找到回文区间)(这里为一层处理)
  27. if isPartition(s,startIndex,i){
  28. tmpString=append(tmpString,s[startIndex:i+1])
  29. }else{
  30. continue
  31. }
  32. //递归
  33. backTracking(s,tmpString,i+1,res)
  34. //回溯
  35. tmpString=tmpString[:len(tmpString)-1]
  36. }
  37. }
  38. //判断是否为回文
  39. func isPartition(s string,startIndex,end int)bool{
  40. left:=startIndex
  41. right:=end
  42. for ;left<right;{
  43. if s[left]!=s[right]{
  44. return false
  45. }
  46. //移动左右指针
  47. left++
  48. right--
  49. }
  50. return true
  51. }

93. 复原 IP 地址

题目描述

image.png

思路

该题和前一道题分割字符串类似,使用回溯搜索法把所有可能性搜出来。思路图如下:
image.png
startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。
本题我们还需要一个变量pointNum,记录添加逗点的数量。
递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.),同时记录分割符的数量pointNum 要 +1。
回溯的时候,就将刚刚加入的分隔符. 删掉就可以了,pointNum也要-1。

运行代码

  1. func restoreIpAddresses(s string) []string {
  2. var res,path []string
  3. backTracking(s,path,0,&res)
  4. return res
  5. }
  6. func backTracking(s string,path []string,startIndex int,res *[]string){
  7. //终止条件
  8. if startIndex==len(s)&&len(path)==4{
  9. tmpIpString:=path[0]+"."+path[1]+"."+path[2]+"."+path[3]
  10. *res=append(*res,tmpIpString)
  11. }
  12. for i:=startIndex;i<len(s);i++{
  13. //处理
  14. path:=append(path,s[startIndex:i+1])
  15. if i-startIndex+1<=3&&len(path)<=4&&isNormalIp(s,startIndex,i){
  16. //递归
  17. backTracking(s,path,i+1,res)
  18. }else {//如果首尾超过了3个,或路径多余4个,或前导为0,或大于255,直接回退
  19. return
  20. }
  21. //回溯
  22. path=path[:len(path)-1]
  23. }
  24. }
  25. func isNormalIp(s string,startIndex,end int)bool{
  26. checkInt,_:=strconv.Atoi(s[startIndex:end+1])
  27. if end-startIndex+1>1&&s[startIndex]=='0'{//对于前导 0的IP(特别注意s[startIndex]=='0'的判断,不应该写成s[startIndex]==0,因为s截取出来不是数字)
  28. return false
  29. }
  30. if checkInt>255{
  31. return false
  32. }
  33. return true
  34. }

78. 子集

题目描述

image.png

思路

组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:
image.png
本题其实就是把所有的点都遍历,每次取完一个就要往数组中添加。而且重复元素不可以加进去。

运行代码

  1. func subsets(nums []int) [][]int {
  2. var res [][]int
  3. sort.Ints(nums)
  4. backtracking(0,nums,[]int{},&res)
  5. return res
  6. }
  7. func backtracking(start int,nums []int,path []int,res *[][]int){
  8. temp := make([]int,len(path))
  9. copy(temp,path)
  10. *res = append(*res,temp)
  11. for i:=start;i<len(nums);i++{
  12. if i >0&&nums[i] == nums[i-1]{
  13. continue
  14. }
  15. path = append(path,nums[i])
  16. backtracking(i+1,nums,path,res)
  17. path = path[:len(path)-1]
  18. }
  19. }

90. 子集 II

题目描述

image.png

思路

这道题目和上一题的区别就是集合里有重复元素了,而且求取的子集要去重。
用示例中的[1, 2, 2] 来举例,如图所示: (注意去重需要先对集合排序
image.png
从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!

运行代码

  1. func subsetsWithDup(nums []int) [][]int {
  2. var res [][]int
  3. sort.Ints(nums)
  4. backtracking(0,nums,[]int{},&res)
  5. return res
  6. }
  7. func backtracking(start int,nums []int,path []int,res *[][]int){
  8. temp := make([]int,len(path))
  9. copy(temp,path)
  10. *res = append(*res,temp)
  11. for i:=start;i<len(nums);i++{
  12. if i >start&&nums[i] == nums[i-1]{
  13. continue
  14. }
  15. path = append(path,nums[i])
  16. backtracking(i+1,nums,path,res)
  17. path = path[:len(path)-1]
  18. }
  19. }

从代码中可以看出,和上一题唯一不同的地方就是第12行,一个是大于0一个是大于start。
大于0:因为i除了第一次为0后面都不为0,也就说明,不管是同一层还是同一个树枝,都不允许后面元素重复。如下图,这里不仅仅在同一层中规定了不允许取元素2不能重复取,还规定了取1取2之后不可以再取下一层的2。这里适用于要求数组中不能包含重复数字。
大于start:只规定了,同一层不可以取相同的元素。
当然,前提都是要对数组进行排序。sort.Ints(nums)
回溯算法 - 图18

491. 递增子序列

题目描述

image.png

思路

而本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。
所以不能使用之前的去重逻辑!
本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。
为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:
image.png
本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。

  • 终止条件

本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题!(opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
在图中可以看出,同一父节点下的同层上使用过的元素就不能在使用了,这里因为不可以提前排序,所以不可以用nums[ i ] == nums[ i-1 ]来判断,只能使用哈希表来判断。也就是同一层中相同元素不可以重复选择。

运行代码

  1. func findSubsequences(nums []int) [][]int {
  2. var subRes []int
  3. var res [][]int
  4. backTring(0,nums,subRes,&res)
  5. return res
  6. }
  7. func backTring(startIndex int,nums,subRes []int,res *[][]int){
  8. if len(subRes)>1{
  9. tmp:=make([]int,len(subRes))
  10. copy(tmp,subRes)
  11. *res=append(*res,tmp)
  12. }
  13. history:=[201]int{}//记录本层元素使用记录
  14. for i:=startIndex;i<len(nums);i++{
  15. //分两种情况判断:
  16. //一,当前取的元素小于子集的最后一个元素,则继续寻找下一个适合的元素
  17. //或者二,当前取的元素在本层已经出现过了,所以跳过该元素,继续寻找
  18. if len(subRes)>0&&nums[i]<subRes[len(subRes)-1]||history[nums[i] + 100]==1{
  19. continue
  20. }
  21. history[nums[i] + 100]=1//表示本层该元素使用过了
  22. subRes=append(subRes,nums[i])
  23. backTring(i+1,nums,subRes,res)
  24. subRes=subRes[:len(subRes)-1]
  25. }
  26. }

以上代码需要注意的是,哈希表的定义和去重。

46. 全排列

题目描述

image.png

思路

排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:
image.png
当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。
而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次
排列问题的不同:

  • 每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了

    运行代码

    1. var res [][]int
    2. func permute(nums []int) [][]int {
    3. res = [][]int{}
    4. backTrack(nums,len(nums),[]int{})
    5. return res
    6. }
    7. func backTrack(nums []int,numsLen int,path []int) {
    8. if len(nums)==0{
    9. p:=make([]int,len(path))
    10. copy(p,path)
    11. res = append(res,p)
    12. }
    13. for i:=0;i<numsLen;i++{
    14. cur:=nums[i]
    15. path = append(path,cur)
    16. nums = append(nums[:i],nums[i+1:]...)//直接使用切片
    17. backTrack(nums,len(nums),path)
    18. nums = append(nums[:i],append([]int{cur},nums[i:]...)...)//回溯的时候切片也要复原,元素位置不能变
    19. path = path[:len(path)-1]
    20. }
    21. }

    47. 全排列 II

    题目描述

    image.png

    思路

    今天有点累,不想写思路了。。。

    运行代码

    ```go var res [][]int func permute(nums []int) [][]int { res = [][]int{} backTrack(nums,len(nums),[]int{}) return res } func backTrack(nums []int,numsLen int,path []int) { if len(nums)==0{

    1. p:=make([]int,len(path))
    2. copy(p,path)
    3. res = append(res,p)

    } used := [21]int{}//跟前一题唯一的区别,同一层不使用重复的数。关于used的思想carl在递增子序列那一题中提到过 for i:=0;i<numsLen;i++{

    1. if used[nums[i]+10]==1{
    2. continue
    3. }
    4. cur:=nums[i]
    5. path = append(path,cur)
    6. used[nums[i]+10]=1
    7. nums = append(nums[:i],nums[i+1:]...)
    8. backTrack(nums,len(nums),path)
    9. nums = append(nums[:i],append([]int{cur},nums[i:]...)...)
    10. path = path[:len(path)-1]

    }

} ```