本文最后更新于:13 天前
                  
                
              
            
            
              
                
                回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。
 
1 理论 回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案。
1.1 解决的问题 
组合问题:N 个数里面按一定规则找出 k 个数的集合 
切割问题:一个字符串按一定规则有几种切割方式 
子集问题:一个 N 个数的集合里有多少符合条件的子集 
排列问题:N 个数按一定规则全排列,有几种排列方式 
棋盘问题:N 皇后,解数独等等 
 
1.2 回溯法模板 
回溯函数模板返回值以及参数 
回溯函数终止条件 
回溯搜索的遍历过程 
 
for 循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了。
1 2 3 4 5 6 7 8 9 10 11 12 func  backtracking (参数)   {     if  (终止条件) {         存放结果         return      }     for  (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {         处理节点         backtracking(路径,选择列表)          回溯,撤销处理结果     } }
 
2 题目 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按任何顺序返回答案。
最直接的解法是使用 k 个 for 循环,但是当 k 过大时这个方法显然不适用。所以回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像 for 循环嵌套 k 层让人绝望。
用树形结构来理解回溯:
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度。每次搜索到了叶子节点,我们就找到了一个结果。
递归函数的返回值以及参数  
 
在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。
1 2 var  path []int var  res  [][]int 
 
函数里一定有两个参数,既然是集合 n 里面取 k 个数,那么 n 和 k 是两个 int 型的参数。
然后还需要一个参数,为 int 型变量 start,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,…,n])。start 用于防止出现重复的组合。
1 var  backTrace func (n, k, start int )   
 
回溯函数终止条件  
 
path 这个数组的大小如果达到 k,说明我们找到了一个子集大小为 k 的组合了,在图中 path 存的就是根节点到叶子节点的路径。
此时用 res 二维数组,把 path 保存起来,并终止本层递归。
1 2 3 4 5 6 if  len (path) == k {      tmp := make ([]int , k)     copy (tmp, path)     res = append (res, tmp)     return  }
 
注意不能直接 append(res, path),后续修改 path 时会影响到 res。 
单层搜索的过程  
 
回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出 for 循环用来横向遍历,递归的过程是纵向遍历。
for 循环每次从 start 开始遍历,然后用 path 保存取到的节点 i。
1 2 3 4 5 6 for  start <= n {                     path = append (path, start)       backTrace(n, k, start + 1 )       path = path[:len (path)-1 ]        start++ }
 
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func  combine (n int , k int )   [][]int  {     var  path []int      var  res [][]int      var  backTrace func (a, b, start int )        backTrace = func (a, b, start int )   {                if  len (path) == b {              tmp := make ([]int , b)             copy (tmp, path)             res = append (res, tmp)             return          }                  for  start <= a {             if  a - start + 1  < b - len (path) {                  break              }             path = append (path, start)             backTrace(a, b, start + 1 )             path = path[:len (path)-1 ]             start++         }     }     backTrace(n, k, 1 )     return  res }
 
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。解集不能包含重复的组合。
这道题的难点在于去重。要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
used[i - 1] == true,说明同一树枝 candidates[i - 1] 使用过,是进入下一层递归 
used[i - 1] == false,说明同一树层 candidates[i - 1] 使用过,是回溯而来的 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func  combinationSum2 (candidates []int , target int )   [][]int  {     sort.Ints(candidates)     var  res [][]int      var  path []int      used := make ([]bool , len (candidates))     var  sum int      var  backtracking func (start int )      backtracking = func (start int )   {         if  sum > target {             return          } else  if  sum == target {             tmp := make ([]int , len (path))             copy (tmp, path)             res = append (res, tmp)         }         for  i := start; i < len (candidates); i++ {             if  i > 0  && candidates[i] == candidates[i-1 ] && !used[i-1 ] {                 continue              }             sum += candidates[i]             path = append (path, candidates[i])             used[i] = true              backtracking(i+1 )             sum -= candidates[i]             path = path[:len (path)-1 ]             used[i] = false          }     }     backtracking(0 )     return  res }
 
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
切割问题本质上和组合问题是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func  partition (s string )   [][]string  {     var  res [][]string      var  path []string      var  backtracking func (sub string )        backtracking = func (sub string )   {         if  len (sub) == 0  {             tmp := make ([]string , len (path))             copy (tmp, path)             res = append (res, tmp)         }         for  i := 1 ; i <= len (sub); i++ {             if  ifStr(sub[0 :i]) {                 path = append (path, sub[0 :i])                 backtracking(sub[i:])                 path = path[:len (path)-1 ]             }         }     }     backtracking(s)     return  res }func  ifStr (s string )   bool  {     b := []byte (s)     for  i, j := 0 , len (b)-1 ; i < j; i, j = i+1 , j-1  {         if  b[i] != b[j] {             return  false          }     }     return  true  }
 
给你一个整数数组 nums,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func  subsetsWithDup (nums []int )   [][]int  {     sort.Ints(nums)     var  res [][]int      var  path []int      used := make ([]bool , len (nums))     var  backtracking func (start int )        backtracking = func (start int )   {         tmp := make ([]int , len (path))         copy (tmp, path)         res = append (res, tmp)         for  i := start; i < len (nums); i++ {             if  i > 0  && nums[i] == nums[i-1 ] && !used[i-1 ] {                 continue              }             path = append (path, nums[i])             used[i] = true              backtracking(i+1 )             path = path[:len (path)-1 ]             used[i] = false          }     }     backtracking(0 )     return  res }
 
给你一个整数数组 nums,找出并返回所有该数组中不同的递增子序列,数组中可能含有重复元素。
不能对数组进行排序,在每一层新建一个 map 进行去重。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func  findSubsequences (nums []int )   [][]int  {     var  res [][]int      var  path []int      var  backtracking func (start int )        backtracking = func (start int )   {         if  len (path) >= 2  {             tmp := make ([]int , len (path))             copy (tmp, path)             res = append (res, tmp)         }         usedmap := make (map [int ]bool )         for  i := start; i < len (nums); i++ {             if  (len (path) != 0  && nums[i] < path[len (path)-1 ]) || usedmap[nums[i]] {                 continue              }             path = append (path, nums[i])             usedmap[nums[i]] = true              backtracking(i+1 )             path = path[:len (path)-1 ]         }     }     backtracking(0 )     return  res }
 
给定一个可包含重复数字的序列 nums,按任意顺序返回所有不重复的全排列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func  permuteUnique (nums []int )   [][]int  {     sort.Ints(nums)     var  res [][]int       var  path []int      var  backtracking func ()        used := make ([]bool , len (nums))     backtracking = func ()   {         if  len (path) == len (nums) {             tmp := make ([]int , len (path))             copy (tmp, path)             res = append (res, tmp)             return          }         for  i := 0 ; i < len (nums); i++ {                          if  used[i] || (i > 0  && nums[i] == nums[i-1 ] && !used[i-1 ]) {                 continue              }             path = append (path, nums[i])             used[i] = true              backtracking()             path = path[:len (path)-1 ]             used[i] = false          }     }     backtracking()     return  res }
 
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 func  canPartitionKSubsets (nums []int , k int )   bool  {     var  sum int      for  _, v := range  nums {         sum += v     }     if  sum % k != 0  {         return  false      }     target := sum / k     sort.Ints(nums)     used := make ([]bool , len (nums))     var  dfs func (start, sum, count int , used []bool )   bool      dfs = func (start, sum, count int , used []bool )   bool  {         if  count == k {             return  true          } else  if  sum == target {             return  dfs(len (nums)-1 , 0 , count+1 , used)         }         for  i := start; i >= 0 ; i-- {             if  used[i] || sum + nums[i] > target {                 continue              }              used[i] = true              if  dfs(i-1 , sum+nums[i], count, used) {                 return  true              }             used[i] = false              if  sum == 0  {                 return  false              }         }         return  false      }     return  dfs(len (nums)-1 , 0 , 0 , used) }
 
N 皇后。
解数独。
3 递归 在这里整理几道不是二叉树的递归题目。
合并两个排序的链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func  mergeTwoLists (l1 *ListNode, l2 *ListNode)   *ListNode {     if  l1 == nil  {         return  l2     } else  if  l2 == nil  {         return  l1     }     var  l *ListNode     if  l1.Val <= l2.Val {         l = l1         l1 = l1.Next     } else  {         l = l2         l2 = l2.Next     }     l.Next = mergeTwoLists(l1, l2)     return  l }
 
3.2 字节面试题 计算字符串表达式,字符串包含数字、括号、加法、乘法,例如:”2+3*(4+5)”。
递归 + 栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func  calculateExpression (expr string )   int  { 	stack := []int {} 	op := '.'  	for  i := 0 ; i < len (expr); i++ { 		if  expr[i] != '+'  && expr[i] != '*'  { 			num := 0  			if  expr[i] >= '0'  && expr[i] <= '9'  { 				for  i < len (expr) && expr[i] >= '0'  && expr[i] <= '9'  { 					num = num*10  + int (expr[i]-'0' ) 					i++ 				} 				i-- 			} else  if  expr[i] == '('  { 				cnt := 1  				j := i + 1  				for  cnt > 0  { 					if  expr[j] == '('  { 						cnt++ 					} else  if  expr[j] == ')'  { 						cnt-- 					} 					j++ 				} 				num = calculateExpression(expr[i+1  : j-1 ]) 				i = j - 1  			} 			if  op == '*'  { 				num *= stack[len (stack)-1 ] 				stack[len (stack)-1 ] = num 			} else  { 				stack = append (stack, num) 			} 		} 		if  expr[i] == '*'  || expr[i] == '+'  { 			op = rune (expr[i]) 		} 	} 	result := 0  	for  _, val := range  stack { 		result += val 	} 	return  result }
 
4 图论 直接看题吧,参考卡哥教程 
所有可能的路径。
使用深度优先搜索遍历图即可,由于题目限制不用考虑环的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func  allPathsSourceTarget (graph [][]int )   [][]int  {     var  res [][]int      var  path []int      dst := len (graph) - 1      var  dfs func (node int )      dfs = func (node int )   {         if  node == dst {             path = append (path, dst)             tmp := make ([]int , len (path))             copy (tmp, path)             res = append (res, tmp)             return          }         path = append (path, node)         for  i := 0 ; i < len (graph[node]); i++ {             nxt := graph[node][i]             dfs(nxt)             path = path[:len (path)-1 ]         }     }     dfs(0 )     return  res }
 
岛屿数量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func  numIslands (grid [][]byte )   int  {     m, n := len (grid), len (grid[0 ])     var  res int      var  mark func (i, j int )        mark = func (i, j int )   {         if  i < 0  || i >= m || j < 0  || j >= n || grid[i][j] == '0'  {             return          }         grid[i][j] = '0'          mark(i-1 , j)         mark(i+1 , j)         mark(i, j-1 )         mark(i, j+1 )     }     for  i := 0 ; i < m; i++ {         for  j := 0 ; j < n; j++ {             if  grid[i][j] == '1'  {                 res++                 mark(i, j)             }         }     }     return  res }
 
课程表。
一道拓扑排序的题,由两种解法:
BFS,维护一个入度表以及边的信息,每次将入度为 0 的边进行排序,并根据边的信息消除它们对其他顶点入度的影响。 
DFS,维护一个 visited 数组用于标记节点状态:0:未访问;1:搜索中;2:已搜索,若 dfs 中再次搜索到 1 的节点,则判断成环。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 func  canFinish (numCourses int , prerequisites [][]int )   bool  {     tomap := make (map [int ][]int )     indeg := make ([]int , numCourses)     for  _, v := range  prerequisites {         tomap[v[1 ]] = append (tomap[v[1 ]], v[0 ])         indeg[v[0 ]]++     }          for  numCourses > 0  {         var  queue []int          for  i := 0 ; i < len (indeg); i++ {             if  indeg[i] == 0  {                                  queue = append (queue, i)                 indeg[i]--             }         }         l := len (queue)         if  l == 0  {             return  false          }         for  i := 0 ; i < l; i++ {             for  j := 0 ; j < len (tomap[queue[i]]); j++ {                                  indeg[tomap[queue[i]][j]]--             }         }         numCourses -= l     }     return  true  }func  canFinish (numCourses int , prerequisites [][]int )   bool  {     res := true      visited := make ([]int , numCourses)     var  dfs func (n int )        dfs = func (n int )   {         if  visited[n] == 2  || !res {             return           } else  if  visited[n] == 1  {             res = false          }         visited[n] = 1          for  _, v := range  prerequisites {             if  v[1 ] == n {                 dfs(v[0 ])             }         }         visited[n] = 2      }     for  i := 0 ; i < numCourses; i++ {         if  visited[i] == 0  && res {             dfs(i)         }     }     return  res }
 
5 并查集 并查集常用来解决连通性问题。当我们需要判断两个元素是否在同一个集合里的时候,就要想到用并查集。
并查集主要有两个功能:
将两个元素添加到一个集合中 
判断两个元素在不在同一个集合 
 
寻找图中是否存在路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func  validPath (n int , edges [][]int , source int , destination int )   bool  {     father := make ([]int , n)          var  find func (n int )   int       find = func (n int )   int  {         if  father[n] == n { return  n }         father[n] = find(father[n])          return  father[n]     }          var  isSame func (u, v int )   bool       isSame = func (u, v int )   bool  {         return  find(u) == find(v)     }          var  join func (u, v int )        join = func (u, v int )   {         u = find(u)         v = find(v)                  if  u == v { return  }         father[v] = u     }          for  i, _ := range  father {         father[i] = i     }     for  _, v := range  edges {         join(v[0 ], v[1 ])     }     return  isSame(source, destination) }