概述 一般可以用回溯法解决以下问题
组合问题:N个数⾥⾯按⼀定规则找出k个数的集合
切割问题:⼀个字符串按⼀定规则有几种切割⽅式
子集问题:⼀个N个数的集合⾥有多少符合条件的子集
排列问题:N个数按⼀定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独
回溯法的问题都可以抽象看为树形结构
for循环可以理解是横向遍历,递归看成纵向遍历 ,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果
1 2 3 4 5 6 7 8 9 10 11 12 void backtracking (参数) { if (终止条件) { 存放结果; return ; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); 回溯,撤销处理结果 } }
组合问题
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 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); public List<List<Integer>> combine (int n, int k) { backtracking(n, k, 1 ); return res; } private void backtracking (int n, int k, int start) { if (temp.size() == k) { res.add(new ArrayList <>(temp)); return ; } for (int i = start; i <= n; i++) { temp.add(i); backtracking(n, k, i + 1 ); temp.removeLast(); } } }
对该代码进行剪枝优化
当n,k都等于4时(则返回1~4中所有4个数的组合)此时只需要进行一次遍历就可以获得结果,其他的遍历都是没有意义的
所以可以在递归的每一层for循环里选择起始位置,将代码进行修改
1 2 3 for (int i = startIndex; i <= n; i++)for (int i = startIndex; i <= n - (k - temp.size()) + 1 ; i++)
temp.size():代表的是,已经选择的元素个数
k - temp.size():代表的是,还需要的元素个数
n - (k - temp.size()) + 1:代表的是,在集合n中至多要从该起始位置进行遍历
暴力回溯 该题与上一题类似,只不过现在需要在[1…9]集合中查找数,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 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); public List<List<Integer>> combinationSum3 (int k, int n) { backtracking(n, 0 , 1 , k); return res; } private void backtracking (int target, int sum, int start, int k) { if (temp.size() == k) { if (sum == target) { res.add(new ArrayList <>(temp)); return ; } } for (int i = start; i <= 9 ; i++) { temp.add(i); sum += i; backtracking(target, sum, i + 1 , k); temp.removeLast(); sum -= i; } } }
剪枝优化 通过题目可以发现当选取元素之和已经超过目标值时,后面的组合遍历就没有意义了,同时for循环的范围和上一题类似,也可以进行剪枝,具体代码如下:
1 2 3 4 if (sum > target) { return ; }
1 2 3 for (int i = start; i <= 9 ; i++)for (int i = start; i <= 9 - (k - temp.size()) + 1 ; i++)
暴力回溯 递归的终止只有两种情况,sum大于target和sum等于target
sum等于target的时候,需要收集结果
注意:进行重复选取
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 class Solution { public List<List<Integer>> combinationSum (int [] candidates, int target) { List<List<Integer>> res = new ArrayList <>(); Deque<Integer> temp = new ArrayDeque <>(); backtracking(res, temp,candidates,target,0 ,0 ); return res; } private void backtracking (List<List<Integer>> res, Deque<Integer> temp, int [] candidates, int target, int sum, int start) { int length = candidates.length; if (sum > target) { return ; } if (sum == target) { res.add(new ArrayList <>(temp)); return ; } for (int i = start; i < length; i++) { sum += candidates[i]; temp.add(candidates[i]); backtracking(res, temp, candidates, target, sum, i); sum -= candidates[i]; temp.removeLast(); } } }
剪枝优化 也可以将上题中创建分支时做减法,改成创建分支时做加法,更方便理解
此时递归结束的判断依据为:sum大于target
但是对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回,其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了
所以,可以对本来的 candidates[] 数组进行排序,排序后,在递归前进行判断,如果下一层的sum(sum + candidates[i])已经大于target,就直接结束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 29 30 31 32 33 34 35 36 37 38 39 40 class Solution { public List<List<Integer>> combinationSum (int [] candidates, int target) { List<List<Integer>> res = new ArrayList <>(); List<Integer> temp = new ArrayList <>(); Arrays.sort(candidates); backtracking(res, temp,candidates,target,0 ,0 ); return res; } private void backtracking (List<List<Integer>> res, List<Integer> temp, int [] candidates, int target, int sum, int start) { int length = candidates.length; if (sum == target){ res.add(new ArrayList <>(temp)); return ; } for (int i = start; i < length; i++) { if (sum + candidates[i] > target){ break ; } temp.add(candidates[i]); backtracking(res, temp, candidates, target, sum + candidates[i], i); temp.remove(temp.size()-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 class Solution { List res = new ArrayList <>(); StringBuilder temp = new StringBuilder (); public List<String> letterCombinations (String digits) { if (digits.length() == 0 ) { return res; } String[] numMapping = new String []{ "" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" }; backtracking(digits, numMapping, 0 ); return res; } private void backtracking (String digits, String[] numMapping, int startNum) { if (startNum == digits.length()) { res.add(temp.toString()); return ; } String str = numMapping[digits.charAt(startNum) - '0' ]; for (int i = 0 ; i < str.length(); i++) { temp.append(str.charAt(i)); backtracking(digits, numMapping, startNum + 1 ); temp.deleteCharAt(temp.length() - 1 ); } } }
将回溯隐藏在递归中 在上述解法中,可以发现,需要取出字符串中的每一个字符,并将他们拼接成字符串,回溯时再将该字符删除掉,回到原本的状态
所以该字符串temp其实没变化过,可以在递归函数中,额外定义一个字符串s。当递归时,将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 33 34 35 class Solution { List res = new ArrayList <>(); public List<String> letterCombinations (String digits) { if (digits.length() == 0 ) { return res; } String[] numMapping = new String []{ "" , "" , "abc" , "def" , "ghi" , "jkl" , "mno" , "pqrs" , "tuv" , "wxyz" }; backtracking(digits, numMapping, 0 , "" ); return res; } private void backtracking (String digits, String[] numMapping, int startNum, String s) { if (startNum == digits.length()) { res.add(s); return ; } String str = numMapping[digits.charAt(startNum) - '0' ]; for (int i = 0 ; i < str.length(); i++) { backtracking(digits, numMapping, startNum + 1 ,s+str.charAt(i)); } } }
递归终止条件为:sum > target 和 sum == target
注意:该题与39. 组合总和 不同的是,candidates中的每个数字只能使用一次,而且candidates内包含有重复元素,结果不能包含重复的元素,所以该题目需要进行去重操作
使用数组进行去重 可以使用一个used数组来进行去重的判断,将选取后的数字所对应的used数组标记为true
注意:要去重的是同一树层上使用过的数字,而不是同一树枝上的重复元素,要去重的是出现的相同组合,而不是一个组合里面的相同元素
在同一树枝上的都是一个组合里的元素
在同一个树层上的为不同组合的元素
在for循环遍历中,如果candidates[i] == candidates[i - 1] && used[i - 1] == false
就说明:前一个树枝选取的元素和当前树枝选取的元素一样,当前已经重复选取了
注意:此处必须要添加used[i - 1] == true
这个条件,如果不加上这个条件,则表示的是同一个树枝里的一个组合
因为在同一个树层中used[i - 1] == false
才能表示当前取的candidates[i]
是从candidates[i - 1]
回溯而来的,而used[i - 1] == true
,说明是进入下一层递归,去下一个数,所以是树枝上,我们不需要去重一个组合里面的相同元素。
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 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); boolean [] used; public List<List<Integer>> combinationSum2 (int [] candidates, int target) { used = new boolean [candidates.length]; Arrays.sort(candidates); backtracking(candidates, target, 0 , 0 ); return res; } private void backtracking (int [] candidates, int target, int start, int sum) { if (sum == target) { res.add(new ArrayList <>(temp)); } for (int i = start; i < candidates.length; i++) { if (sum + candidates[i] > target) { break ; } if (i > 0 && candidates[i] == candidates[i - 1 ] && !used[i - 1 ]) { continue ; } used[i] = true ; temp.add(candidates[i]); backtracking(candidates, target, i + 1 , sum + candidates[i]); used[i] = false ; temp.removeLast(); } } }
不使用数组进行去重 candidates[i] == candidates[i - 1]
这个条件控制的仅仅是前后元素是否相同,如果单纯使用这个条件就会导致同一树枝出现的相同元素也会去重,不符合题目要求
加上了i > strat
这个条件后,如果在同一个树枝就会发现,i==start
这个条件是始终满足的,只有在同一个树层时才会出现i>start
这种情况,所以该条件可以对同一个树层使用过的元素进行去重,而不会错误地将同一个树枝的元素进行去重
1 2 3 if (i > strat && candidates[i] == candidates[i - 1 ]) { continue ; }
完整代码如下:
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 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); public List<List<Integer>> combinationSum2 (int [] candidates, int target) { Arrays.sort(candidates); backtracking(candidates, target, 0 , 0 ); return res; } private void backtracking (int [] candidates, int target, int start, int sum) { if (sum == target) { res.add(new ArrayList <>(temp)); } for (int i = start; i < candidates.length; i++) { if (sum + candidates[i] > target) { break ; } if (i > start && candidates[i] == candidates[i - 1 ]) { continue ; } temp.add(candidates[i]); backtracking(candidates, target, i + 1 , sum + candidates[i]); temp.removeLast(); } } }
分割问题
切割问题与组合问题类似,组合问题通过在数组里面获取数字,而切割问题则是在数组里面截取数字,所以需要定义一条分割线。分割线其实与组合问题一样,都表示下一轮递归的起始位置
终止条件为:起始位置已经大于字符串s的长度,此时说明找到了一组切割的方案,所以终止*
判断是否回文:采用双指针的方法对字符串进行检验
截取字符串:在循环for (int i = startIndex; i < s.size(); i++)
中,在判断子串为回文后,可以通过String substring = s.substring(start, i + 1);
进行切割
注意:切割过的位置不能重复切割,所以在递归的时候,使用backtracking(s, 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 33 34 35 36 37 38 39 40 41 42 class Solution { List<List<String>> res = new ArrayList <>(); LinkedList<String> temp = new LinkedList <>(); public List<List<String>> partition (String s) { backtracking(s, 0 ); return res; } private void backtracking (String s, int start) { if (start >= s.length()) { res.add(new ArrayList (temp)); return ; } for (int i = start; i < s.length(); i++) { if (isPalindrome(s, start, i)) { String substring = s.substring(start, i + 1 ); temp.add(substring); } else { continue ; } backtracking(s, i + 1 ); temp.removeLast(); } } private boolean isPalindrome (String str, int left, int right) { for (int i = left, j = right; i < j; i++, j--) { if (str.charAt(i) != str.charAt(j)) { return false ; } } return true ; } }
pointNum:记录逗点的数量
终止条件:当pointNum的值等于3时,说明字符串已被分成4段,此时只需要判断第四段字符串是否合法,如果合法,则说明该切割组合符合题目要求,加入到结果中即可
截取字符串:在for循环遍历字符串中for (int i = start; i < s.length(); i++)
,[start, i]就是截取的子字符串,我们需要判断当前字符串是否合法,如果合法,就在截取的子字符串后面加上逗点,然后再进行递归回溯
注意:因为切割后的字符串后加上了逗点,所以下次递归的起始位置应该是当前位置+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 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 class Solution { List<String> res = new ArrayList <>(); public List<String> restoreIpAddresses (String s) { backTracking(s,0 ,0 ); return res; } private void backTracking (String s, int start, int pointNum) { if (pointNum == 3 ){ if (isLegal(s,start,s.length()-1 )){ res.add(s); } return ; } for (int i = start; i < s.length(); i++) { if (isLegal(s,start,i)){ s = s.substring(0 ,i+1 )+"." +s.substring(i+1 ); backTracking(s, i+2 , pointNum+1 ); s=s.substring(0 ,i+1 )+s.substring(i+2 ); }else { break ; } } } private boolean isLegal (String s, int start, int end) { if (start > end) { return false ; } if (s.charAt(start) == '0' && start != end) { return false ; } int num = 0 ; for (int i = start; i <= end; i++) { if (s.charAt(i)>'9' ||s.charAt(i)<'0' ){ return false ; } num = num*10 +(s.charAt(i) - '0' ); if (num>255 ){ return false ; } } return true ; } }
子集问题
分割问题和组合问题都是收集树的叶子结点,而子集问题是找树的所有结点
因为要收集所有的结点,所以需要在递归函数的最开始对元素进行收集
注意:因为子集是无序的。{1, 2, 3}与{2, 3, 1}是一样的,所以取过的元素不会重复取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); public List<List<Integer>> subsets (int [] nums) { backTracking(nums,0 ); return res; } private void backTracking (int [] nums,int start) { res.add(new ArrayList <>(temp)); for (int i = start; i < nums.length; i++) { temp.add(nums[i]); backTracking(nums,i+1 ); temp.removeLast(); } } }
该题与40. 组合总和 II 类似,因为原数组包含重复元素,所以该题需要对结果进行去重(具体思路参考40. 组合总和 II )
先对原数组进行排序
去重针对同一树层的元素
去重不针对同一树枝的元素
使用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 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); boolean [] used; public List<List<Integer>> subsetsWithDup (int [] nums) { used = new boolean [nums.length]; Arrays.sort(nums); backTracking(nums, 0 , used); return res; } private void backTracking (int [] nums, int start, boolean [] used) { res.add(new ArrayList <>(temp)); if (start >= nums.length) { return ; } for (int i = start; i < nums.length; i++) { if (i > 0 && nums[i] == nums[i - 1 ] && !used[i - 1 ]) { continue ; } used[i] = true ; temp.add(nums[i]); backTracking(nums, i + 1 , used); temp.removeLast(); used[i] = false ; } } }
不使用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 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); public List<List<Integer>> subsetsWithDup (int [] nums) { Arrays.sort(nums); backTracking(nums, 0 ); return res; } private void backTracking (int [] nums, int start) { res.add(new ArrayList <>(temp)); if (start >= nums.length) { return ; } for (int i = start; i < nums.length; i++) { if (i > start && nums[i] == nums[i - 1 ]) { continue ; } temp.add(nums[i]); backTracking(nums, i + 1 ); temp.removeLast(); } } }
排列问题
终止条件:改题类似求子集问题,遍历树形结构找每一个节点,但是题目要求递增子序列大小至少为2,所以当临时数组temp.size()
大于1的时,就可以将该组合进行收集
该题的重点是,在同一个数层 中,如果出现了两个相同的元素,那么不能重复使用,不然会导致结果出现重复情况,所以可以通过一个map来进行去重,在遍历中先在map中进行判断,如果该元素在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 26 27 28 29 30 class Solution { List<List<Integer>> res = new ArrayList <>(); LinkedList<Integer> temp = new LinkedList <>(); public List<List<Integer>> findSubsequences (int [] nums) { backTracking(nums, 0 ); return res; } private void backTracking (int [] nums, int start) { if (temp.size() > 1 ) { res.add(new ArrayList <>(temp)); } HashMap<Integer, Integer> map = new HashMap <>(); for (int i = start; i < nums.length; i++) { if (!temp.isEmpty() && nums[i] < temp.getLast()) { continue ; } if (map.getOrDefault(nums[i], 0 ) >= 1 ) { continue ; } map.put(nums[i], map.getOrDefault(nums[i], 0 ) + 1 ); temp.add(nums[i]); backTracking(nums, i + 1 ); temp.removeLast(); } } }
棋盘问题 待补充
其他 待补充