算法-回溯集合

147 阅读4分钟

什么问题

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

模板-三部曲

  • 返回值 参数
  • 终止条件
  • 回溯搜索的遍历过程

for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

组合问题

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

回溯剪枝

剪枝:如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

20210130194335207.png

var res [][]int
func combine(n int, k int) [][]int {
    res=[][]int{}
    if n<=0 || k<=0 ||k>n{
        return res
    }
    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)
        backtrack(n,k,i+1,track)
        track = track[:len(track)-1]
    }
}

组合总和3

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

  • 本题k相当于了树的深度,9(因为整个集合就是9个数)就是树的宽度。
func combinationSum31(k int, n int) [][]int {
	var res [][]int
	var path []int
	sum := 0
	var backTrack func(int)
	backTrack = func(start int){
		// 剪枝,如果当前路径和已经大于n或者路径长度已经大于k,后续遍历就没有意义了
		if sum > n || len(path) > k{
			return
		}
		// 递归终止条件
		// 如果当前路径长度为k,且路径和等于n,便找到了一条满足要求的路径
		if len(path) == k && sum == n{
			temp := make([]int, k)
			copy(temp, path)
			res = append(res, temp)
		    return
		}
		// for循环遍历(剪枝)
		for i:=start;i<=9-(k-len(path))+1;i++{
			// 处理每一个节点
			sum += i
			path = append(path, i)
			// 递归
			backTrack(i+1)
			// 回溯
			sum -= i
			path = path[:len(path)-1]
		}
	}
	backTrack(1)
	return res
}

电话号码的数字组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

  1. 数字和字母如何映射
  2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
  3. 输入1 * #按键等等异常情况
func letterCombinations(digits string) []string {
    length := len(digits)
    if length==0 || length>4{
        return nil
    }
    digitsMap := [10]string{
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    }
    res := []string{}
    var tempstring string 
    var backtrack func(index int)
    backtrack = func(index int){
        if len(tempstring) == len(digits){
           var copystring string
            copystring = tempstring
            res = append(res,copystring)
            return
        }

        tmpk := digits[index]-'0'
        letter := digitsMap[tmpk]
        for i:=0;i<len(letter);i++{
            tempstring = tempstring+string(letter[i])
            backtrack(index+1)
            tempstring = tempstring[:len(tempstring)-1]
        }
    }
    backtrack(0)

    return res

}

组合总和--可以重复选取

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

func combinationSum1(candidates []int, target int) [][]int {
   sum := 0
   temp := []int{}
   res := [][]int{}
   var backtrack func(start int)
   backtrack = func(start int){
      if sum > target{
         return
      }
      if sum == target{
         copytemp := make([]int,len(temp))
         copy(copytemp,temp)
         res = append(res,copytemp)
         return
      }

      for i:= start;i<len(candidates);i++{
         temp = append(temp,candidates[i])
         sum += candidates[i]
         backtrack(i)
         sum-=candidates[i]
         temp = temp[:len(temp)-1]
      }
   }
   backtrack(0)
   return res
}

组合总和2---只能使用一次。

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

先排序

两点:

  1. 本层取过就不能取了
  2. 树枝上要传入i+1
//两个维度去重复,横向for去重,先排序;纵向通过start
func combinationSum2(candidates []int, target int) [][]int {
    sum := 0
    track := []int{}
    res := [][]int{}
    sort.Ints(candidates)
    var backtrack func(start int)
    backtrack = func(start int){
        if sum >target{
            return
        }
        if sum == target{
            temp := make([]int,len(track))
            copy(temp,track)
            res =append(res,temp)
            return
        }

        for i:=start;i<len(candidates);i++{
            if i > start && candidates[i]==candidates[i-1]{
                continue    
            }    
            track = append(track,candidates[i])
            sum += candidates[i]
            backtrack(i+1)
            sum-=candidates[i]
            track = track[:len(track)-1]
        }
    }
    backtrack(0)
    return res
}

分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回 s 所有可能的分割方案。

两点:

  1. 切割问题,有不同的切割方式
  2. 判断回文

131.分割回文串.jpg

 func isPartition(s string,start,end int) bool {
	left := start
	right := end
	for left < right{
		if s[left] == s[right]{
            left++
		    right--
		}else{
            return false
        }
	}
	return true
}

func partition(s string) [][]string {
    res := make([][]string,0)
    var temp []string
    var backtrack func(start int)
    backtrack = func(start int){
        if start == len(s){
            t := make([]string,len(temp))
            copy(t,temp)
            res = append(res,t)
        }
        for i:=start;i<len(s);i++{
            if isPartition(s,start,i){
                temp = append(temp,s[start:i+1])
            }else{
                continue
            }
            backtrack(i+1)
            temp = temp[:len(temp)-1]
        }
    }
    backtrack(0)
    return res

}

复原ip地址

给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。

有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.'

20201123203735933.png

  • 判断字符串符不符合ip条件

终止条件 start = len(s)

  • 另外注意是先加入path,再去怕判断长度是不是超了,如果超了那就直接返回上一层,注意上一层的path是没有加入新元素的,也就是说还是len=4的path,然后更新path len=3
func isNormalIP(s string,start,end int)bool  {
	if end-start+1 > 1 && s[start]=='0'{
		return false
	}
	checkInt,_ := strconv.Atoi(s[start:end+1])
	if checkInt > 255 {
		return false
	}
	for i :=start;i<=end;i++{
		if s[i] > '9' || s[i] <'0' {
			return false
		}
	}

	return true
}
func restoreIpAddresses(s string)[]string  {
	var res,path []string

	var backTracking2 func(start int,path []string)
	backTracking2 = func(start int,path []string){
		if start == len(s) && len(path) == 4{
			copypath := make([]string,len(path))
			copy(copypath,path)
			res = append(res ,string(strings.Join(copypath,".")) )
		}

		for i:=start;i<len(s);i++{
			path = append(path,s[start:i+1])
			
			if i-start+1<=3 && isNormalIP(s,start,i)&&len(path)<=4{
				backTracking2(i+1,path)
			}else {
				//如果首尾超过了3个,或路径多余4个,或前导为0,或大于255,直接回退
				return
			}
			path = path[:len(path)-1]
		}
	}
	backTracking2(0,path)
	return res
}


子集

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例: 输入: nums = [1,2,3] 输出: [ [3],   [1],   [2],   [1,2,3],   [1,3],   [2,3],   [1,2],   [] ]

  • 如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!

202011232041348.png

///先放一个空集进去
func subsets(nums []int) [][]int {
    res := make([][]int,0)
    temp := []int{}
    var backtrack func(start int)
    backtrack = func(start int){
        tmp := make([]int,len(temp))
        copy(tmp,temp)
        res = append(res,tmp)

        for i:=start;i<len(nums);i++{
            temp =append(temp,nums[i])
            backtrack(i+1)
            temp = temp[:len(temp)-1]
        }

    }
    sort.Ints(nums)
    backtrack(0)
    return res

}

子集2-集合里有重复元素,求取的子集要去重

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

-排序

20201124195411977.png

func subsetsWithDup(nums []int) [][]int {
    var temp []int
    res := make([][]int,0)
    var backtrack func(start int)
    backtrack = func(start int){
        tmp := make([]int,len(temp))
        copy(tmp,temp)
        res = append(res,tmp)
        if start == len(nums){
            return
        }
        for i:=start;i<len(nums);i++{
            if i> start &&nums[i]==nums[i-1]{
                continue
            }
            temp = append(temp,nums[i])
            backtrack(i+1)
            temp = temp[:len(temp)-1]
        }
    }
    sort.Ints(nums)
    backtrack(0)
    return res

}


递增子序列 -- 不可以排序

给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。

不能有相同的递增子序列。

20201124200229824.png

func findSubsequences(nums []int) [][]int {
    var res [][]int
    backTring(0,nums,[]int{},&res)
    return res
}

func backTring(start int,nums,subRes []int,res *[][]int)  {
	if len(subRes)>1 {
		tmp := make([]int, len(subRes))
		copy(tmp, subRes)
		*res = append(*res, tmp)
	}
    //注意她在每一层都要开辟一个history去记录这一层的信息,有没有重复
	//history := [201]int{}
    history  := make(map[int]bool)
	for i :=start;i<len(nums);i++{
		//分两种情况判断:一,当前取的元素小于子集的最后一个元素,则继续寻找下一个适合的元素
		//二,当前取的元素在本层已经出现过了,所以跳过该元素,继续寻找
		if len(subRes)>0 && nums[i]<subRes[len(subRes)-1] || history[nums[i]] {
			continue
		}
		history[nums[i]] = true
		subRes = append(subRes,nums[i])
		backTring(i+1,nums,subRes,res)
		subRes = subRes[:len(subRes)-1]


	}
}

全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

  • 输入: [1,2,3]
  • 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

20211027181706.png

func permute1(nums []int) [][]int {
    visited := make(map[int]bool)
    res := [][]int{}
    var backtrack func(path []int)
    backtrack = func(path []int){
        if len(nums) == len(path){
            p := make([]int,len(path))
            copy(p,path)
            res = append(res,p)
            return
        }
        for i:=0;i<len(nums);i++{
            if visited[nums[i]]{
                continue
            }
            cur := nums[i]
            path = append(path,cur)
            visited[nums[i]] = true   
            backtrack(path)
            visited[nums[i]] = false
            path = path[:len(path)-1]

        }
    }
    backtrack([]int{})
    return res
}

全排列2 --包含重复数字---去重复排序

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有包含重复数字

20201124201331223.png

  • 还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了

  • i从0开始,所以nums[i] == nums[i-1]可能是同一层也可能是同一树枝上 // 同树枝的时候会满足nums[i] == nums[i-1],但是如果visied[i-1]=true就证明是同树,是可以的

func permuteUnique(nums []int) [][]int {
 visited := make(map[int]bool)
    res := [][]int{}
    var backtrack func(path []int)
    backtrack = func(path []int){
        if len(nums) == len(path){
            p := make([]int,len(path))
            copy(p,path)
            res = append(res,p)
            return
        }
        //i从0开始,所以nums[i] == nums[i-1]可能是同一层也可能是同一树枝上
        // 同树枝的时候会满足nums[i] == nums[i-1],但是如果visied[i-1]=true就证明是同树,是可以的
        // 同层的话如果visied[i-1]==false,那么就要continue
        //去重去的是层
        for i:=0;i<len(nums);i++{
           if i>0 && nums[i] == nums[i-1]&& !visited[i-1]{
               continue
           }
            if visited[i]{
                continue
            }
            cur := nums[i]
            path = append(path,cur)
            visited[i] = true   
            backtrack(path)
            visited[i] = false
            path = path[:len(path)-1]

        }
    }
    sort.Ints(nums)
    backtrack([]int{})
    return res
}

n皇后

皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

首先来看一下皇后们的约束条件:

  1. 不能同行
  2. 不能同列
  3. 不能同斜线
func solveNQueens(n int)[][]string{
    bd := make([][]string,n)
    for i:=0;i<len(bd);i++{
        bd[i] = make([]string,n)
        
        for j :=0;j<len(bd[i]);j++{
            bd[i][j] = "."
        }
    
    }
    
    res := [][]string{}
    //r 代表着行
    var helper func(r int)
    helper = func(r int){
        if r== n{
            //tmp存的是字符串切片,每一位代表一行
            tmp := make([]string,n)
            for i:=0;i<len(dp);i+{
                tmp[i] = strings.Join(bs[i],"")
            }
            res = append(res,tmp)
        }
        
        for col:=0;col<n;col++{
            if !isVakid(bd,r,col){
                continue
            }
            bd[r][col] = "Q"
	    helper1(r+1)
	    bd[r][col] = "."
        
        }
    
    
    }
    helper(0)
    return res
}
func isVakid(bd [][]string,row,col int)bool{
    //lie
    for i:=0;i<row;i++{
        if bd[row][col]=="Q"{
            return false
        }
     }
        
     for i,j:=row-1,col-1;i>=0&&j>=0;i,j = i-1,j-1{
         if bd[i][j] = "Q"{
                return false
          }
     }     
     for i,j := row-1,col+1;i>=0&&j<len(bd);i,j = i-1,j+1{
         if bd[i][j] = "Q"{
                return false
          }
     }
}

解数独

编写一个程序,通过填充空格来解决数独问题。

数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 '.' 表示。

func solveSudoku(board [][]byte)  {

	full(board)
}
//两个for循环去遍历棋盘
//不用设置终止条件,应为只有一个解,不符合的直接返回false回溯
func full(board [][]byte)bool  {
	for i :=0;i<9;i++{
		for j:=0;j<9;j++{
			//判断此位置是否合适填数字
			if board[i][j] != '.'{
				continue
			}
			//尝试1-9
			for k:='1';k<='9';k++{
				if isvalid(i,j ,byte(k),board)==true{
					board[i][j] = byte(k)
					if full(board) == true{
						return true
					}
					board[i][j] = '.'
				}
			}
			return false
		}
	}
	
	return true
}

func isvalid(row,col int,k byte,board [][]byte)bool  {
	for i:=0;i<9;i++{//行
		if board[row][i]==k{
			return false
		}
	}
	for i:=0;i<9;i++{//列
		if board[i][col]==k{
			return false
		}
	}

	//方格  计算方格左上起点(row/3)*3(col/3)*3
	startrow:=(row/3)*3
	startcol:=(col/3)*3
	for i:=startrow;i<startrow+3;i++{
		for j:=startcol;j<startcol+3;j++{
			if board[i][j]==k{
				return false
			}
		}
	}
	return true
}