回溯算法解决问题最有规律性,借用一下卡哥的图:
只要遇到上述问题就可以考虑使用回溯,回溯法的效率并不高,是一种暴力解法,其代码是嵌套在for循环中的递归,用来解决暴力算法解决不了的问题,即可以通过回溯控制递归的层数,递归后可以进行回溯操作,这样下一次循环就不会收到上一次的影响。解决回溯问题的关键就在于清楚整个回溯递归过程,毕竟是嵌套在for循环中的递归,最好的办法就是把所有情况画成一个树,这样就比较清晰了。
递归通用模版和常用API
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
常用API
- temp常用LinkedList,因为可以在回溯的时候快速删除最后一个元素,添加使用add,删除使用removeLast
- 将temp添加进result时,要new一个新的arrayList将temp传入构造器,因为temp是全局共享的,如果不new那么result里没有值
- 如果需要下一层递归指向下一个数,那么要加参数startIndex,如果是排列问题等,每次从头开始,就不需要了
回溯简单示例
力扣题目:
第77题. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
暴力for循环是没法解决本题的,因为不知道要写多少层,所以使用回溯法。
本题图解:
本题代码:
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> temp = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
backTracking(n,k,1);
return result;
}
public void backTracking(int n,int k,int startIndex){
if(temp.size() == k){
result.add(new ArrayList<Integer>(temp));
return;
}
for(int i=startIndex;i<=n-(k-temp.size())+1;i++){
temp.add(i);
backTracking(n,k,i+1);
temp.removeLast(); // 回溯
}
}
}
去重问题
去重是使用回溯算法时遇到的最重要的问题,卡哥将去重分为了树枝去重以及数层去重,就是for循环的去重和各层递归的去重,一个横向一个纵向。
最常用的去重思路:
先对原数组排序,使用used数组,对横向for循环去重,对纵向递归不去重。
力扣题目:
40. 组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
本题图示:
可以看到只要排序好,进行横向去重就行了。used数组的作用就是去重的时候不要误删纵向情况,因为used数组有回溯操作,下一次for循环的used[i-1]是false了已经,而下一层递归是true,以此为条件进行判断。
class Solution {
// 去重,去重横向的,纵向递归的不去重,要用used[i-1]防止纵向的被去重的时候误删
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> temp = new LinkedList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
if(candidates == null && candidates.length == 0){
return null;
}
Arrays.sort(candidates);
boolean[] used = new boolean[candidates.length];
backtracking(candidates,target,0,0,used);
return result;
}
public void backtracking(int[] candidates,int target,int sum,int startIndex,boolean[] used){
if(sum == target){
result.add(new ArrayList<>(temp));
return;
}
for(int i=startIndex;i<candidates.length && sum+candidates[i]<=target;i++){
if(i>0 && candidates[i] == candidates[i-1] && used[i-1] == false){
continue;
}
sum += candidates[i];
temp.add(candidates[i]);
used[i] = true;
backtracking(candidates,target,sum,i+1,used);
used[i] = false;
temp.removeLast();
sum -= candidates[i];
}
}
}
还有用used数组去重不了的情况
Set去重
力扣题目:
491. 递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
这道题显然是不能用used数组的,因为没法排序,排序后就判断不了递增子序列了。使用hashset或者数组进行去重
class Solution {
// 不能排序的去重,使用hashset
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> temp = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums,0);
return result;
}
public void backTracking(int[] nums,int startIndex){
if(judge(temp)){
result.add(new ArrayList<>(temp));
}
HashSet<Integer> set = new HashSet<>();
for(int i= startIndex;i<nums.length;i++){
if(set.contains(nums[i])){
continue;
}
set.add(nums[i]);
temp.add(nums[i]);
backTracking(nums,i+1);
temp.removeLast();
}
}
public boolean judge(List<Integer> list){
if(list.size()<2){
return false;
}
for(int i=0,j=0;j<list.size()-1;i++){
j=i+1;
if(list.get(i)>list.get(j)){
return false;
}
}
return true;
}
}
每层递归都有自己的hastSet,然后可以对横向for循环去重,因为每一层递归set都不一样,所以不会影响到纵向递归。使用数组同理,和set差不多。
其他问题
排列和组合的区别:
排列是每次递归都是从头开始的:
力扣题目:
46. 全排列
使用used数组是来判断这个元素有没有在上层递归使用过,毕竟每次都是从0开始
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> temp = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] used =new boolean[nums.length];
backTracking(nums,used);
return result;
}
public void backTracking(int[] nums,boolean[] used){
if(temp.size() == nums.length){
result.add(new ArrayList<>(temp));
return;
}
for(int i=0;i<nums.length;i++){
if(used[i] == true){
continue;
}
temp.add(nums[i]);
used[i] = true;
backTracking(nums,used);
used[i] = false;
temp.removeLast();
}
}
}
78.子集和90.子集II属于子集问题,这类问题的特点就是要采集树上每一个出现过的元素,不难,去重逻辑也是上面那样。
还有一些拼接字符串的,需要用StringBuilder和一些stringApi的题目:
131.分割回文串和93.复原IP地址 不算难但是比较恶心。
比较难的就是:
51. N皇后
需要遍历矩阵,但是只需要一个for循环,判定条件比较多
52. 解数独
需要双层循环,判定条件也比较复杂