回溯法解决的问题 回溯法,一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
组合是不强调元素顺序的,排列是强调元素顺序 。
例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。
回溯模板
1 2 3 4 5 6 7 8 9 10 11 12 void backtracking (参数) { if (终止条件) { 存放结果; return ; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking (路径,选择列表); 回溯,撤销处理结果 } }
回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。
如图:
关于递归传参startIndex问题
同个集合来求组合的话,就需要startIndex;多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
一个集合中的元素不能重复选取类型(递归参数需要传入startIndex,表示下一轮递归遍历的起始位置)
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex递归的参数
多个集合 中的元素不能重复选取类型。(不需要startIndex)
一个集合中的元素可以无限重复选取,但是有总和的限制。(横向遍历不能重复取,需要startIndex,但是纵向遍历可以重复读取当前的数,所以递归的时候传参i)
一个集合中的元素只能使用一次,并且数组是有重复的元。(需要startIndex,并且要先排序数组,然后进行剪枝)
剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够 题目要求的k个元素了,就没有必要搜索了 。
在求和问题中,排序之后加剪枝是常见的套路!
关于去重问题
回溯算法:递增子序列的去重是同一个父节点下的本层的去重。
回溯算法:求子集问题(二)的去重是要整颗树的本层去重,但是整棵树的同一层去重不好操作,所以才排序,与前一个树枝对比就可以了。
组合问题 一个集合中的元素不能重复选取类型(递归参数需要传入startIndex,表示下一轮递归遍历的起始位置) 组合 Leetcode:77.组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
思路:递归来做层叠嵌套(可以理解是写k层for循环),每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了 。
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 var ( path []int result [][]int ) func combine (n int , k int ) [][]int { path, result = make ([]int , 0 , k), make ([][]int , 0 ) backtracking(n, k, 1 ) return result } func backtracking (n, k, startIndex int ) { if len (path) == k { tmp := make ([]int , k) copy (tmp, path) result = append (result, tmp) return } for i := startIndex; i <= n; i++ { if n - i + len (path) + 1 < k { break } path = append (path, i) backtracking(n, k, i+1 ) path = path[:len (path)-1 ] } }
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
组合总和III Leetcode:216.组合总和III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]]
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 var ( path []int res [][]int ) func combinationSum3 (k int , n int ) [][]int { path, res = make ([]int , 0 , k), make ([][]int , 0 ) backtracking(n, k, 0 , 1 ) return res } func backtracking (n, k, sum, startIndex int ) { if sum == n && len (path) == k { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) return } for i := startIndex; i <= 9 ; i++ { if 9 - i + len (path) + 1 < k || sum + i > n { break } path = append (path, i) sum += i backtracking(n, k, sum, i+1 ) path = path[:len (path)-1 ] sum -= i } }
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
多个集合中的元素不能重复选取类型。(不需要startIndex) 电话号码的字母组合 Leetcode:17.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射与电话按键相同。注意 1 不对应任何字母。
示例:
输入:”23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
思路:因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,与之前在同一个集合中的求组合不同。所以注意这里for循环是从0开始的,并不是从startIndex的;当前中的index代表digits中的第几个数字。
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 var ( m []string path []byte res []string ) func letterCombinations (digits string ) []string { m = []string {"" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" } path, res = make ([]byte , 0 , len (digits)), make ([]string , 0 ) if digits == "" { return res } backtracking(digits, 0 ) return res } func backtracking (digits string , index int ) { if len (path) == len (digits) { tmp := make ([]byte , len (path)) copy (tmp, path) res = append (res, string (tmp)) return } digit := digits[index] - '0' str := m[digit] for i := 0 ; i < len (str); i++ { path = append (path, str[i]) backtracking(digits, index+1 ) path = path[:len (path)-1 ] } }
时间复杂度: O(3^m * 4^n),其中 m 是对应四个字母的数字个数,n 是对应三个字母的数字个数
空间复杂度: O(3^m * 4^n)
一个集合中的元素可以无限重复选取,但是有总和的限制。(横向遍历不能重复取,需要startIndex,但是纵向遍历可以重复读取当前的数,所以递归的时候传参i) 组合总和 Leetcode:39.组合总和
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为: [ [7], [2,2,3] ]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
思路分析:与前面组合问题一样,唯一不同的地方在于集合中的数字可以无限制重复被选取 ,也就是纵向遍历可以重复选取,横向遍历不能重复选取(如果横向遍历重复选取,会出现重复的组合),所以需要用到startIndex,但是递归传参时传递的不是i+1
,而是i
,传递i
,代表可以重复选择集合中的元素;其次,在求和问题中,排序之后加剪枝是常见的套路! ,所以还需要在递归前进行排序。
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 var ( path []int res [][]int ) func combinationSum (candidates []int , target int ) [][]int { path, res = make ([]int , 0 , len (candidates)), make ([][]int , 0 ) sort.Ints(candidates) backtracking(candidates, 0 , target, 0 ) return res } func backtracking (candidates []int , sum, target int , startIndex int ) { if sum > target { return } if sum == target { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) return } for i := startIndex; i < len (candidates); i++ { path = append (path, candidates[i]) sum += candidates[i] backtracking(candidates, sum, target, i) path = path[:len (path)-1 ] sum -= candidates[i] } }
时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
空间复杂度: O(target)
一个集合中的元素只能使用一次,并且数组是有重复的元。(需要startIndex,并且要先排序数组,然后进行剪枝) 组合总和 II Leetcode:40.组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
1 2 3 4 5 6 7 8 输入: candidates = [10,1,2,7,6,1,5], target = 8, 输出: [ [1,1,6], [1,2,5], [1,7], [2,6] ]
思路:本类型题目与前面题目区别在于集合(数组candidates)有重复元素,但还不能有重复的组合 。所以在此基础上,需要对集合进行去重,也就是对树结构中同一层进行去重,去重前首先要排序,保证集合是有序的 。
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 var ( path []int res [][]int ) func combinationSum2 (candidates []int , target int ) [][]int { path, res = make ([]int , 0 , len (candidates)), make ([][]int , 0 ) sort.Ints(candidates) backtracking(candidates, 0 , target, 0 ) return res } func backtracking (candidates []int , sum, target int , startIndex int ) { if sum > target { return } if sum == target { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) return } for i := startIndex; i < len (candidates); i++ { if i > startIndex && candidates[i] == candidates[i-1 ] { continue } path = append (path, candidates[i]) sum += candidates[i] backtracking(candidates, sum, target, i+1 ) path = path[:len (path)-1 ] sum -= candidates[i] } }
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
切割问题 其实切割问题类似组合问题 。
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…..。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…..。
分割回文串 Leetcode:131.分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
1 2 输入:s = "aab" 输出:[["a","a","b"],["aa","b"]]
示例 2:
列出如下几个难点:
思路:关于模拟切割线,其实就是startIndex是上一层已经确定了的分割线,i是这一层试图寻找的新分割线
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 var ( path []string res [][]string ) func partition (s string ) [][]string { path, res = make ([]string , 0 , len (s)), make ([][]string , 0 ) backtracking(s, 0 ) return res } func backtracking (s string , startIndex int ) { if startIndex >= len (s) { tmp := make ([]string , len (path)) copy (tmp, path) res = append (res, tmp) return } for i := startIndex; i < len (s); i++ { str := s[startIndex:i+1 ] if isPalindrome(str) { path = append (path, str) backtracking(s, i+1 ) path = path[:len (path)-1 ] } } } func isPalindrome (s string ) bool { for i, j := 0 , len (s)-1 ; i < j; i, j = i+1 , j-1 { if s[i] != s[j] { return false } } return true }
时间复杂度: O(n * 2^n)
空间复杂度: O(n^2)
子集问题 在树形结构中子集问题是要收集所有节点的结果,而组合问题、分割问题是收集叶子节点的结果,所以一般来说递归不用加终止条件,让for循环遍历完整棵树,然后终止。
不需要去重,收集树中每个节点的值 子集 Leetcode:78.子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例: 输入: nums = [1,2,3] 输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
思路:收集每一个节点的值,所以没有返回条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var ( path []int res [][]int ) func subsets (nums []int ) [][]int { path, res = make ([]int , 0 , len (nums)), make ([][]int , 0 ) backtracking(nums, 0 ) return res } func backtracking (nums []int , startIndex int ) { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) for i := startIndex; i < len (nums); i++ { path = append (path, nums[i]) backtracking(nums, i+1 ) path = path[:len (path)-1 ] } }
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
需要去重, 先排序再去重剪枝 类似组合问题中的去重,先排序数组,然后进行剪枝,即当前元素与前一个元素相同,则当前元素的分支比与前一个元素分支相同。
子集II Leetcode:90.子集II
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]
思路:增加去重逻辑
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 var ( path []int res [][]int ) func subsetsWithDup (nums []int ) [][]int { path, res = make ([]int , 0 , len (nums)), make ([][]int , 0 ) sort.Ints(nums) backtracking(nums, 0 ) return res } func backtracking (nums []int , startIndex int ) { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) for i := startIndex; i < len (nums); i++ { if i > startIndex && nums[i] == nums[i-1 ] { continue } path = append (path, nums[i]) backtracking(nums, i+1 ) path = path[:len (path)-1 ] } }
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
自增子序列,去重,求子集 递增子序列 Leetcode:491.递增子序列
给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。
示例:
输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]
说明:
给定数组的长度不会超过15。
数组中的整数范围是 [-100,100]。
给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。
思路:该题由于是求集合中的递增子序列,所以不能用对原集合先排序后去重的方法,所以去重逻辑不仅仅是与本层中前一个节点进行对比去重,而是要与同一个父节点下本层中所有之前的节点 进行对比去重,用哈希表set进行去重。
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 var ( path []int res [][]int ) func findSubsequences (nums []int ) [][]int { path, res = make ([]int , 0 , len (nums)), make ([][]int , 0 ) backtracking(nums, 0 ) return res } func backtracking (nums []int , startIndex int ) { if len (path) > 1 { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) } set := make (map [int ]struct {}, 0 ) for i := startIndex; i < len (nums); i++ { if _, ok := set[nums[i]]; ok { continue } if len (path) == 0 || nums[i] >= path[len (path)-1 ] { set[nums[i]] = struct {}{} path = append (path, nums[i]) backtracking(nums, i+1 ) path = path[:len (path)-1 ] } } }
时间复杂度: O(n * 2^n)
空间复杂度: O(n)
排列问题
每层都是从0开始搜索而不是startIndex
需要used数组记录path里都放了哪些元素了
不需要去重 全排列 Leetcode:46.全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
思路:排列问题,每次都要从头开始搜索,所以不需要startIndex,而used数组,其实就是记录此时path里都有哪些元素使用了(记录元素位置),一个排列里一个元素只能使用一次 (正是由于每次都要从头开始搜索集合,所以才需要used数组)。
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 var ( path []int res [][]int ) func permute (nums []int ) [][]int { path, res = make ([]int , 0 , len (nums)), make ([][]int , 0 ) used := make (map [int ]struct {}, 0 ) backtracking(nums, used) return res } func backtracking (nums []int , used map [int ]struct {}) { if len (nums) == len (path) { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) return } for i := 0 ; i < len (nums); i++ { if _, ok := used[i]; ok { continue } used[i] = struct {}{} path = append (path, nums[i]) backtracking(nums, used) delete (used, i) path = path[:len (path)-1 ] } }
需要去重 全排列 II Leetcode:47.全排列 II
给定一个可包含重复数字 的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
1 2 3 4 5 输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]
示例 2:
1 2 输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
提示:
1 <= nums.length <= 8
-10 <= nums[i] <= 10
思路:需要先对集合进行排序,判断当前的值是否使用过,再对同一个层相同的两个分支进行去重。
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 var ( path []int res [][]int ) func permuteUnique (nums []int ) [][]int { path, res = make ([]int , 0 , len (nums)), make ([][]int , 0 ) used := make (map [int ]struct {}, 0 ) sort.Ints(nums) backtracking(nums, used) return res } func backtracking (nums []int , used map [int ]struct {}) { if len (nums) == len (path) { tmp := make ([]int , len (path)) copy (tmp, path) res = append (res, tmp) return } for i := 0 ; i < len (nums); i++ { if _, ok := used[i]; ok { continue } if _, ok := used[i-1 ]; ok && i > 0 && nums[i] == nums[i - 1 ]{ continue } used[i] = struct {}{} path = append (path, nums[i]) backtracking(nums, used) delete (used, i) path = path[:len (path)-1 ] } }
时间复杂度: O(n! * n)
空间复杂度: O(n)