目录
回溯算法概念
关于回溯算法你要知道
回溯算法适用题型
回溯模板
组合问题
力扣 77 组合
力扣 39 组合总和(同一个元素可以无限重复次的取)
力扣 40 组合总和Ⅱ
力扣 216 组合总和Ⅲ
力扣 17 电话号码的字母组合 (多个集合求组合)
排列问题
力扣 46 全排列
力扣 47 全排列Ⅱ(包含重复元素)
子集问题
力扣 78 子集
力扣 90 子集Ⅱ(包含重复元素)
力扣 491 递增子序列
切割问题
力扣 131 分割回文串
棋盘问题
力扣 51 N皇后
其他问题
力扣 93 复原IP地址
1. 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
2. 回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。
3. 许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
4. 在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
5. 若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
6. 而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
7. 除过深度优先搜索,常用的还有广度优先搜索。
回溯算法听上去就很高大上,很不好理解,有时候光听名字就劝退了一部分同学,但其实回溯算法并不是什么高效的算法。回溯算法的本质就是穷举所有的可能,然后我们选出我们想要的结果。穷举所有的可能?这一听就非常多,又占用时间,又占用内存,所以我们通过剪枝来使回溯算法高效一点。(这也是唯一能优化回溯算法的方法)。回溯算法其实也是一种暴力算法,最多再剪枝一下。那我们为什么学这又难又不高效的算法呢?因为这也是没办法的,很多题能用暴力搜索出来就不错了,最多再剪枝一下。
所有回溯法的问题都可以抽象为树形结构。因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。递归就要有终止条件,所以必然是一颗高度有限的树(N叉树)。(横向遍历,纵向递归)
public void dfs(参数)
{
if(满足终止条件)
{
存放结果
return;
}
for(遍历本层集合中的元素)
{
处理结点;
dfs(参数);//递归
撤销处理该结点; //回溯
}
}
1. 组合问题:N个数里面按照一定规则找出K个数的组合
import java.util.*;
/**组合
* @Author liusifan
* @Data 2022/4/18 16:06
*/
public class dfsTest {
public List> combine(int n, int k)
{
List> ret=new ArrayList<>();
Deque path=new LinkedList<>();
dfs(ret,path,n,k,1);
return ret;
}
public void dfs(List> ret, Deque path, int n,int k,int startIndex)
{
//满足终止条件
if (path.size()==k)
{
//把结果添加到结果集
ret.add(new ArrayList<>(path));
return;
}
//剪枝
//已选择的元素个数:path.size();
//还需要选择的元素个数:k-path.size();
//在集合中最多从该位置起始 n-(k-path.size()+1开始遍历
for (int i=startIndex;i<=n-(k-path.size())+1;i++)
{
path.addLast(i);//处理该节点
dfs(ret, path, n, k, i+1);//递归
path.removeLast();//回溯
}
}
}
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 组合总和
* @Author liusifan
* @Data 2022/4/2 11:06
*/
public class Num39
{
List> ret=new ArrayList<>();
List path=new ArrayList<>();
public List> combinationSum(int[] candidates, int target)
{
Arrays.sort(candidates);//需要排序
// sum 当前累加的和,startIndex 下一层for循环的起始位置
backtrack(candidates,target,0,0);
return ret;
}
private void backtrack(int[] candidates, int target, int sum, int startIndex)
{
//收集元素已经大于目标元素,没必要进行递归了。
if (sum>target)
{
return;
}
//终止条件,符合要求
if (target==sum)
{
//把结果添加到结果集中
ret.add(new ArrayList<>(path));
return;
}
// sum+candidates[i]<=target 在这里进行了剪枝,如果当前元素加上已收集的元素和大于target了就可以结束本轮元素的遍历
for (int i=startIndex;i> ret=new ArrayList<>();
// Deque path=new LinkedList<>();
// public List> combinationSum(int[] candidates, int target)
// {
// Arrays.sort(candidates);//需要排序
// // sum 当前累加的和,startIndex 下一层for循环的起始位置
// backtrack(candidates,target,0,0);
// return ret;
// }
// private void backtrack(int[] candidates, int target, int sum, int startIndex)
// {
// //收集元素已经大于目标元素,没必要进行递归了。
// if (sum>target)
// {
// return;
// }
// //终止条件,符合要求
// if (target==sum)
// {
// //把结果添加到结果集中
// ret.add(new LinkedList<>(path));
// return;
// }
// // sum+candidates[i]<=target 在这里进行了剪枝,如果当前元素加上已收集的元素和大于target了就可以结束本轮元素的遍历
// for (int i=startIndex;i
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 组合总和Ⅱ
* @Author liusifan
* @Data 2022/4/3 13:06
*/
public class Num40
{
List> ret=new ArrayList<>();
List path=new ArrayList<>();
public List> combinationSum2(int[] candidates, int target)
{
if (candidates.length==0)
{
return ret;
}
//先排序(考虑重复元素一定要先排序)
Arrays.sort(candidates);
backtrack(candidates,target,0,0);
return ret;
}
private void backtrack(int[] candidates, int target, int sum, int startIndex)
{
if (sum==target)
{
ret.add(new ArrayList<>(path));
return;
}
//剪枝:如果sum+candidates[i]>目标和就跳过,不走循环
for (int i=startIndex;istartIndex判读是否同一层,i=startIndex同一层第一个
//只能是candidates[i]==candidates[i-1],candidates[i+1]就越界了,
if (i>startIndex&&candidates[i]==candidates[i-1])
{
continue;
}
sum+=candidates[i];
path.add(candidates[i]);
//每个元素只能出现一次,不能重复取,要在下个位置 所以 i+1
backtrack(candidates,target,sum,i+1);
sum-=candidates[i];
path.remove(path.size()-1);
}
}
}
import java.util.ArrayList;
import java.util.List;
/**
* 组合总和Ⅲ
* @Author liusifan
* @Data 2022/4/2 10:20
*/
public class Num216
{
List> ret=new ArrayList<>();
List path=new ArrayList<>();
public List> combinationSum3(int k, int n)
{
backtracking(k ,n,0,1);
return ret;
}
//targetSum 目标和
//sum 已经相加的元素和
//startIndex 下一次循环的搜索的起始位置
private void backtracking(int k, int targetSum, int sum, int startIndex)
{
//结果长度等于k个数
if (path.size()==k)
{
//目标和等于相加的元素
if (sum==targetSum)
{
ret.add(new ArrayList<>(path));
}
return;
}
//当i的值大于目标和时,已经没必要继续遍历了,不进入循环(剪枝)
for (int i=startIndex;i<=9&&i<=targetSum;i++)
{
sum+=i;
path.add(i);
//每个只能使用一次,不能重复,则下次遍历从当前元素(i)的下个元素(i+1)开始
backtracking(k,targetSum,sum,i+1);
sum-=i;//回溯
path.remove(path.size()-1);//回溯
}
}
}
用树形的形式表示出来如下:
此题和上面几个题不同的是,上面几道题都是一个集合求组合,此题是多个集合求组合,每一个数字代表的是不同的集合,也就是求不同集合之间的组合。因此for循环就不是从startIndex开始遍历了。而是每次从头开始遍历。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 电话号码的字母组合
* @Author liusifan
* @Data 2022/4/2 14:31
*/
public class Num17
{
List ret=new ArrayList<>();
StringBuilder s=new StringBuilder();
public List letterCombinations(String digits)
{
if (digits.length()==0)
{
return ret;
}
Map map = new HashMap(){{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
backtrack(map,digits,0);
return ret;
}
public void backtrack(Map map,String digits, int index)
{
if (index==digits.length())
{
ret.add(s.toString());
return;
}
//index记录遍历第几个数字
//首先取index指向的数字,并找到对应的字符集
char c = digits.charAt(index);
String letter = map.get(c);
for (int i = 0; i
2.排列问题:N个数按照一定规律全排列,有几种排列方式。
这里提醒一下排列和组合的区别:
在排列中{1,2}和{2,1}是两个集合。
而组合中是一个集合。
即:排列是有顺序的,组合是没有顺序的。
到了排序问题,就和组合不一样了。排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。
但是在排序过程中需要一个used数组,来记录path中已经选择过的元素。一个排列里一个元素只能使用一次。
import java.util.*;
/**全排列
* @Author liusifan
* @Data 2022/4/18 16:06
*/
public class dfsTest {
public List> permute(int[] nums)
{
List> ret=new ArrayList<>();
Deque path=new LinkedList<>();
//used数组收录哪些元素使用过了,一个排列元素不能重复
boolean[] used=new boolean[nums.length];
dfs(ret,path,nums,used);
return ret;
}
public void dfs(List> ret, Deque path, int[] nums, boolean[] used)
{
if (path.size()==nums.length)
{
ret.add(new ArrayList<>(path));
return;
}
for (int i=0;i
排序和组合的不同点:
- 每层都是从0开始搜索而不是startIndex
- 需要used数组记录path里都放了哪些元素了
和上一题不同的是,这个题可包含重复元素,既然包含重复元素,我们就要考虑去重的问题。
同一数层不能重复,而且同一树枝只能使用一次。
import java.util.*;
/**全排列Ⅱ
* @Author liusifan
* @Data 2022/4/18 16:06
*/
public class dfsTest {
public List> permuteUnique(int[] nums)
{
List> ret=new ArrayList<>();
Deque path=new LinkedList<>();
//used数组收录哪些元素使用过了,一个排列元素不能重复
boolean[] used=new boolean[nums.length];
Arrays.sort(nums);
dfs(ret,path,nums,used);
return ret;
}
public void dfs(List> ret, Deque path, int[] nums, boolean[] used)
{
if (path.size()==nums.length)
{
//在搜索到符合条件的解的时候,通常会做一个拷贝。
ret.add(new ArrayList<>(path));
return;
}
for (int i=0;i0保证used[i-1]有效
if (i>0&&nums[i-1]==nums[i]&&used[i-1]==false)
{
continue;
}
//如果当前节点没有使用,那就开始处理当前0100111011节点
if (used[i]==false)
{
//正在处理该节点,used[i]=true
used[i] = true;
path.addLast(nums[i]);
dfs(ret, path, nums, used);
used[i] = false; //回溯
path.removeLast(); //回溯
}
}
}
}
3.子集问题:一个N个数的集合里有多少个符合条件的子集。
在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果。
import java.util.ArrayList;
import java.util.List;
/**
* 子集
* @Author liusifan
* @Data 2022/3/29 21:37
*/
public class Num78
{
List> ret=new ArrayList<>();
List path=new ArrayList<>();
public List> subsets(int[] nums)
{
backtrack(nums,0);
return ret;
}
private void backtrack(int[] nums, int startIndex)
{
//将元素添加到结果集
ret.add(new ArrayList<>(path));
for (int i = startIndex; i
每次看到包含重复元素我们都要考虑元素去重的问题 。在元素去重上不仅可以使用一个数组来标记哪些元素使用过,也可以使用Set去重。(set去重相比使用数组记录效率要低的多)此题给出两种解题思路。
方法一:使用Set集合去重 :
import java.util.*;
/**子集Ⅱ
* @Author liusifan
* @Data 2022/4/18 16:06
*/
public class dfsTest
{
Deque result=new LinkedList<>();
Set> ret=new HashSet<>();
public List> subsetsWithDup(int[] nums)
{
//先对原数组进行排序,从而确保所有爆搜出来的方案,都具有单调性
Arrays.sort(nums);
dfs(nums,0);
//使用hset的元素唯一性去重,再转回List>形式(题目要求)
List> ans=new ArrayList<>(ret);
return ans;
}
private void dfs(int[] num, int startIndex)
{
ret.add(new ArrayList<>(result));
for (int i=startIndex;i
方法二:
import java.util.*;
/**子集Ⅱ
* @Author liusifan
* @Data 2022/4/18 16:06
*/
public class dfsTest
{
public List> subsetsWithDup(int[] nums)
{
List> ans=new ArrayList<>();
List result =new ArrayList<>();
Arrays.sort(nums);
dfs(ans,result,nums,0);
return ans;
}
private void dfs(List> ans, List result, int[] nums, int startIndex)
{
ans.add(new ArrayList<>(result));
for (int i=startIndex;istartIndex 确保同一层,i=startIndex同一层
if (i>startIndex&&nums[i]==nums[i-1])
{
continue;
}
result.add(nums[i]);
dfs(ans, result, nums,i+1);
result.remove(result.size()-1);
}
}
}
此题和上题大致相同,不同的是集合中至少有两个元素。 不能有空集合的情况。
注意:此题不能排序,因为排序完已经是自增子序列了。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* 递增子序列
* @Author liusifan
* @Data 2022/4/16 11:35
*/
public class Num491 {
HashSet> ret = new HashSet<>();
List path = new ArrayList<>();
public List> findSubsequences(int[] nums)
{
backtraking(nums, 0);
//使用hashset的元素唯一性去重,再转回List>形式
List> result = new ArrayList<>(ret);
return result;
}
private void backtraking(int[] nums, int startIndex) {
if (path.size() > 1)
{
// 注意这里不要加return,因为要取树上的所有节点
ret.add(new ArrayList<>(path));
}
for (int i = startIndex; i < nums.length; i++)
{
//当前的元素大于前一个的时候,才添加,保证了是递增的,
if (path.size() == 0 || nums[i] >= path.get(path.size() - 1)) {
path.add(nums[i]);
backtraking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* 递增子序列
* @Author liusifan
* @Data 2022/4/16 11:35
*/
public class Num491
{
List temp = new ArrayList();
List> ans = new ArrayList>();
public List> findSubsequences(int[] nums)
{
dfs(0, Integer.MIN_VALUE, nums);
return ans;
}
public void dfs(int cur, int last, int[] nums)
{
if (cur == nums.length)
{
if (temp.size() >= 2)
{
ans.add(new ArrayList(temp));
}
return;
}
if (nums[cur] >= last)
{
temp.add(nums[cur]);
dfs(cur + 1, nums[cur], nums);
temp.remove(temp.size() - 1);
}
if (nums[cur] != last)
{
dfs(cur + 1, last, nums);
}
}
}
4.切割问题:一个字符串按一定规则有几种切割方式。
我们要把切割问题用求解组合问题的思路来解决,怎么模拟切割线,什么时候终止,如何截取子串,还要判断是否回文。
注意:切割过了就不能切割了,下一个开始位置要i+1
import java.util.*;
/**
* 分割回文串
* @Author liusifan
* @Data 2022/4/4 11:22
*/
public class Num131
{
List> ret=new ArrayList<>();
Deque deque=new LinkedList<>();
public List> partition(String s) {
if (s.length() == 0) {
return ret;
}
backtrack(s, 0);
return ret;
}
private void backtrack(String s, int startIndex) {
if (startIndex>=s.length())
{
ret.add(new ArrayList<>(deque));
return;
}
for (int i=startIndex;i
棋盘问题:N皇后
import java.util.*;
/**
* N皇后(回溯) 广度优先遍历(BFS)
* @Author liusifan
* @Data 2022/4/7 18:31
*/
public class Num51
{
List> ret=new ArrayList<>();
public List> solveNQueens(int n)
{
char[][] chessboard=new char[n][n];
//初始化棋盘
for (char[] c:chessboard)
{
Arrays.fill(c,'.');
}
backtracking(n,0,chessboard);
return ret;
}
private void backtracking(int n, int row,char[][] chessboard)
{
//每一行都放了皇后
if (row==n)
{
ret.add(Array2List(chessboard));
return;
}
for (int col=0;col Array2List(char[][] chessboard)
{
List list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
private boolean isVaild(int col, int row, int n, char[][] chessboard)
{
//该列是否有元素
for (int i = 0; i =0&&j>=0;i--,j--)
{
if (chessboard[i][j]=='Q')
{
return false;
}
}
//45度角(检查右上方皇后是否有冲突)
for (int i=row-1,j=col+1;i>=0&&j<=n-1;i--,j++)
{
if (chessboard[i][j]=='Q')
{
return false;
}
}
return true;
}
}
import java.util.ArrayList;
import java.util.List;
/**
* 复原ip地址
* @Author liusifan
* @Data 2022/4/14 18:30
*/
public class Num93
{
List ret=new ArrayList<>();
public List restoreIpAddresses(String s)
{
// 如果字符串长度<4或>12,肯定不是合法ip
if (s.length()<4||s.length()>12)
{
return ret;
}
backtracking(s,0,0);
return ret;
}
private void backtracking(String s, int startIndex,int pointNum)
{
//.号数量等于3的时候,分隔结束
if (pointNum==3)
{
//如果最后一个区间合法,就放进结果集
if (isValid(s,startIndex,s.length()-1))
{
ret.add(s);
}
return;
}
//剩余字符串长度大于剩余最大长度就剪枝
if ((4-pointNum)*3end) {
return false;
}
//不能含有前导0,不合法,如果以0开头,必须只有0
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;
}
//两个ASCII码对应的数字相减
num=num*10+(s.charAt(i)-'0');
if (num>255)
{
return false;
}
}
return true;
}
}