回溯算法
1.回溯算法总结
1.1 回溯算法思路总结
1、最本质的法宝是“画图”,千万不能偷懒,拿纸和笔“画图”能帮助我们更好地分析递归结构,这个“递归结构”一般是“树形结构”,而符合题意的解正是在这个“树形结构”上进行一次“深度优先遍历”,这个过程有一个形象的名字,叫“搜索”; 我们写代码也几乎是“看图写代码”,所以“画树形图”很重要。
2、然后使用一个状态变量,一般我习惯命名为 path、pre ,在这个“树形结构”上使用“深度优先遍历”,根据题目需要在适当的时候把符合条件的“状态”的值加入结果集;
这个“状态”可能在叶子结点,也可能在中间的结点,也可能是到某一个结点所走过的路径。
3、在某一个结点有多个路径可以走的时候,使用循环结构。当程序递归到底返回到原来执行的结点时,“状态”以及与“状态”相关的变量需要“重置”成第 1 次走到这个结点的状态,这个操作有个形象的名字,叫“回溯”,“回溯”有“恢复现场”的意思:意即“回到当时的场景,已经走过了一条路,尝试走下一条路”。
第 2 点中提到的状态通常是一个列表结构,因为一层一层递归下去,需要在列表的末尾追加,而返回到上一层递归结构,需要“状态重置”,因此要把列表的末尾的元素移除,符合这个性质的列表结构就是“栈”(只在一头操作)。
4、当我们明确知道一条路走不通的时候,例如通过一些逻辑计算可以推测某一个分支不能搜索到符合题意的结果,可以在循环中 continue 掉,这一步操作叫“剪枝”。
“剪枝”的意义在于让程序尽量不要执行到更深的递归结构中,而又不遗漏符合题意的解。因为搜索的时间复杂度很高,“剪枝”操作得好的话,能大大提高程序的执行效率。
“剪枝”通常需要对待搜索的对象做一些预处理,例如第 47 题、第 39 题、第 40 题、第 90 题需要对数组排序。“剪枝”操作也是这一类问题很难的地方,有一定技巧性。
总结一下:“回溯” = “深度优先遍历” + “状态重置” + “剪枝”,写好“回溯”的前提是“画图”。
1.2 回溯算法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
1.3 什么时候需要startIndex
针对于组合问题:
1.如果是一个集合来求组合的话,就需要startIndex,例如力扣77,216
2.如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如力扣17
1.4 剪枝
剪枝一般是在for循环里进行的,所谓剪枝,即剪断支路,换句话说是停止递归
所以剪枝是在for循环内的递归函数之前进行
1.5 "树层去重"和“树枝去重”(力扣47总结的最全面)
分析过程代码随想录
1.树层去重:
同一父节点下的同一层出现相同元素(如下图蓝色所示);
如果给定的数组中含有重复元素,要求单个结果可以有重复元素(树枝可以有重复),结果集里不能有相同结果,则树层去重。
例如(1,1,2),如果要求结果为两个元素,则第一条路径可以出现(1,1)(1,2),第二条路径出现(1,2),导致重复,因为第二条路径产生的(1,2)必然能在第一条路径的匹配中产生。
2.树枝去重:随着递归深入形成的一条树枝中出现重复元素
树枝去重分为两类:随着递归深入数组中同一位置的元素本身形成重复(for循环每次从0开始);随着递归深入不同位置的相同元素形成重复(for循环从0和从startIndex开始都存在此问题);分别称为“树枝相同去重”和“树枝不同去重”。
还没见过树枝不同去重;
1.6 组合/排列问题和子集问题的区别
用回溯法解决问题,都可以将分析过程等效成一个树形结构
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
1.7 时间和空间复杂度
时间:指数级别
空间:O(n)
1.8 used数组定义位置与树层和树枝去重的关系
1.如果将used数组定义在递归函数中的for循环之前,实现树层去重
2.如果将used数组定义为全局变量,或是作为递归函数的参数传入,可以同时实现树层和树枝相同去重以及树层不同去重
1.9 回溯的写法
LinkedList.removeLast()
ArrayList.remove(ArrayList.size()-1);
2.组合(力扣77)
一个集合求组合.[1,2,3,4]->[1,2][1,3][1,4][2,3][2,4][3,4]
//1.回溯(未剪枝)
List<List<Integer>> list;
LinkedList<Integer> path;
public List<List<Integer>> combine(int n, int k) {
list = new ArrayList<>();
path = new LinkedList<>();
backTracking(n,k,1);
return list;
}
public void backTracking(int n,int k,int startIndex){
//终止条件
if(path.size() == k){
//添加到“结果集”
list.add(new LinkedList<>(path));
return;
}
for(int i = startIndex;i <= n;i ++){
path.add(i);
backTracking(n,k,i + 1);
//回溯
path.removeLast();
}
}
//2.回溯+剪枝
List<List<Integer>> list;
LinkedList<Integer> path;
public List<List<Integer>> combine(int n, int k) {
list = new ArrayList<>();
path = new LinkedList<>();
backTracking(n,k,1);
return list;
}
public void backTracking(int n,int k,int startIndex){
if(path.size() == k){
list.add(new LinkedList<>(path));
return;
}
//剪枝
for(int i = startIndex;i <= (n - (k - path.size()) + 1);i ++){
path.add(i);
backTracking(n,k,i + 1);
path.removeLast();
}
}
3.组合总和 III(力扣216)
一个集合求组合,k个数和为n
//1.回溯(未剪枝)
List<List<Integer>> list;
LinkedList<Integer> path;
public List<List<Integer>> combinationSum3(int k, int n) {
list = new ArrayList<>();
path = new LinkedList<>();
combina(k,n,1);
return list;
}
public void combina(int k,int n,int startIndex){
if(path.size() == k){
if(n == 0){
list.add(new ArrayList<>(path));
}
return;
}
for(int i = startIndex;i <= 9;i ++){
path.add(i);
combina(k,n - i,i + 1);
path.removeLast();
}
}
//2.回溯+剪枝
List<List<Integer>> list;
LinkedList<Integer> path;
public List<List<Integer>> combinationSum3(int k, int n) {
list = new ArrayList<>();
path = new LinkedList<>();
combina(k,n,1);
return list;
}
//回溯函数,传入组合的个数,还需要多大空缺满足要求(n),本次for循环开始下标
public void combina(int k,int n,int startIndex){
//剪枝,当n<0,没有往下遍历的需要了
if(n < 0) return;
if(path.size() == k){
if(n == 0){
list.add(new ArrayList<>(path));
}
return;
}
//剪枝,当发现即使加上后面剩余的数也不能满足总和为k,剪掉
for(int i = startIndex;i <= 10 - k + path.size();i ++){
path.add(i);
combina(k,n - i,i + 1);
path.removeLast();
}
}
4.电话号码的字母组合(力扣17)
多个集合求组合;注意本题和77,216的区别;本题是不同集合组合,前两个是相同集合组合
//1.回溯
List<String> list;
StringBuilder sb;
public List<String> letterCombinations(String digits) {
list = new ArrayList<>();
sb = new StringBuilder();
int len = digits.length();
if(len == 0){
return null;
}
letter(digits,0);
return list;
}
//startIndex用来记录应该取digits中第几个数了
public void letter(String digits,int startIndex){
if(sb.length() == digits.length()){
list.add(sb.toString());
return;
}
String cur = getString(digits.charAt(startIndex));
//此题是不同集合组合,不需要记录起始下标
for(int i = 0;i < cur.length();i ++){
sb.append(cur.charAt(i));
letter(digits,startIndex + 1);
sb.deleteCharAt(sb.length() - 1);
}
}
//传入字符,返回对应的字符串,当然这里也可以将数字和字符串的对应关系放到数组中
public String getString(char c){
String res;
switch (c){
case '2':
res = "abc";
break;
case '3':
res = "def";
break;
case '4':
res = "ghi";
break;
case '5':
res = "jkl";
break;
case '6':
res = "mno";
break;
case '7':
res = "pqrs";
break;
case '8':
res = "tuv";
break;
case '9':
res = "wxyz";
break;
default:
res = "";
break;
}
return res;
}
5.组合总和(力扣39)
一个集合(无重复元素);可以重复取值,startIndex为i而不是i+1了;
本身有无重复元素决定去重;是否可以重复取值,startIndex为i或是i + 1;
//回溯+剪枝
List<List<Integer>> list;
LinkedList<Integer> path;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
list = new ArrayList<>();
path = new LinkedList<>();
Arrays.sort(candidates);
combination(candidates,target,0);
return list;
}
public void combination(int[] candidates,int target,int startIndex){
if(target < 0) return;
if(target == 0){
list.add(new ArrayList<>(path));
return;
}
for(int i = startIndex;i < candidates.length;i ++){
//剪枝,不过需要所给数组有序
if(target < candidates[i]) break;
path.add(candidates[i]);
//注意这里不是i+1了,填入i表示数组当前位置元素可以重复
combination(candidates,target - candidates[i],i);
path.removeLast();
}
}
6.组合总和 II(力扣40)
一个集合(有重复元素),不可以重复取值
本身有无重复元素决定去重;是否可以重复取值,startIndex为i或是i + 1;
组合问题树层去重,同一父节点下的同一层出现相同元素
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(candidates);
combination(list,new ArrayList<>(),candidates,target,0);
return list;
}
public void combination(List<List<Integer>> list,List<Integer> path,int[] candidates ,int target,int startIndex){
if(target == 0){
list.add(new ArrayList<>(path));
}
for(int i = startIndex;i < candidates.length;i ++){
if(target < candidates[i]) break;
//集合中有重复元素:跳过同一层中的重复元素
//i>startIndex:只看一层中的元素,不与上下层产生关系
if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
path.add(candidates[i]);
//只有一个集合并且不能重复取值,startIndex为i + 1
combination(list,path,candidates,target - candidates[i],i + 1);
path.remove(path.size() - 1);
}
}
7.分割回文串(力扣131)
切割问题,可以等效成组合问题
//回溯
List<List<String>> list;
List<String> path;
public List<List<String>> partition(String s) {
list = new ArrayList<>();
path = new ArrayList<>();
part(s,0);
return list;
}
//startIndex:分割点的起始位置下标
public void part(String s,int startIndex){
//满足下面条件,表示递归到底,此次分割是成功的,将结果加入结果集
if(startIndex >= s.length()){
list.add(new ArrayList<>(path));
return;
}
//利用startIndex和i圈定每次分割子串范围
for(int i = startIndex;i < s.length();i ++){
if(isReverse(s,startIndex,i)){
String str = s.substring(startIndex,i + 1);
path.add(str);
//如果当前分割结果不是回文串,停止此条路径上的递归
}else{
continue;
}
part(s,i + 1);
//回溯
if(path.size() > 0){
path.remove(path.size() - 1);
}
}
}
//使用双指针法来判断一个字符串是否是回文串
public boolean isReverse(String s,int start,int end){
for(int i = start,j = end;i <= j;i ++,j --){
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
8.复原 IP 地址(力扣93)
切割问题,注意细节,break
//回溯
//用来记录最终结果
List<String> list = new ArrayList<>();
//记录每次分割得到的结果
List<String> path = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
restore(s, 0);
return list;
}
public void restore(String s, int startIndex) {
//地址为四段,如果已经超过四段了,直接停止递归
if (path.size() > 4) return;
if (startIndex >= s.length() && path.size() == 4) {
//将path转为string字符串
String str = path.get(0);
for (int i = 1; i < 4; i++) {
str += ".";
str += path.get(i);
}
list.add(str);
}
for (int i = startIndex; i < s.length(); i++) {
//根据startIndex和i截取s,得到字符串
String str = s.substring(startIndex, i + 1);
//当截取的数值在0-255范围内
if (Integer.parseInt(str) >= 0 && Integer.parseInt(str) <= 255) {
//如果出现如:“01”这种非法情况,结束本次循环
if (str.charAt(0) == '0' && str.length() > 1) {
break;
}
path.add(str);
//截取的数值不在要求范围内,没有在递归下去的必要,结束本次循环
} else {
break;
}
restore(s, i + 1);
//回溯
path.remove(path.size() - 1);
}
}
9.子集(力扣78)
一个数组(无重复元素),求出所有子集
子集问题是收集树的所有节点,而前面的组合与分割问题是收集树的所有叶子节点
//1.
List<List<Integer>> list;
List<Integer> path;
public List<List<Integer>> subsets(int[] nums) {
list = new ArrayList<>();
path = new ArrayList<>();
if(nums == null) return list;
sub(nums,0);
return list;
}
public void sub(int[] nums,int startIndex){
if(startIndex > nums.length) return;
//注意这里子集问题和组合,分割问题的区别
//子集问题:收集树的所有节点 组合,分割问题:收集树的所有叶子节点
list.add(new ArrayList<>(path));
for(int i = startIndex;i < nums.length;i ++){
path.add(nums[i]);
sub(nums,i + 1);
path.remove(path.size() - 1);
}
}
//2.
/*
* 因为nums大小不为0,故解集中一定有空集。令解集一开始只有空集,
* 然后遍历nums,每遍历一个数字,拷贝解集中的所有子集,将该数字
* 与这些拷贝组成新的子集再放入解集中即可。时间复杂度为O(n^2)。
* */
public List<List<Integer>> subsets2(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
List<Integer> path = new ArrayList<>();
list.add(new ArrayList<>(path));
for(int i = 0;i < nums.length;i ++){
int size = list.size();
for(int j = 0;j < size;j ++){
List<Integer> temp = new ArrayList<>(list.get(j));
temp.add(nums[i]);
list.add(temp);
}
}
return list;
}
10.子集 II(力扣90)
一个数组(有重复元素),需树层去重;
//回溯
List<List<Integer>> list;
List<Integer> path;
public List<List<Integer>> subsetsWithDup(int[] nums) {
list = new ArrayList<>();
path = new ArrayList<>();
Arrays.sort(nums);
backTracking(nums,0);
return list;
}
public void backTracking(int[] nums,int startIndex){
list.add(new ArrayList<>(path));
for(int i = startIndex;i < nums.length;i ++){
//所给集合中含有重复元素,为了避免结果重复,要做“树层去重”
if(i > startIndex && nums[i] == nums[i - 1]){
continue;//注意这里使用continue
}
path.add(nums[i]);
backTracking(nums,i + 1);
path.remove(path.size() - 1);
}
}
11.递增子序列(力扣491)
一个数组(有重复元素),树层去重;
List<List<Integer>> list;
List<Integer> path;
public List<List<Integer>> findSubsequences(int[] nums) {
list = new ArrayList<>();
path = new ArrayList<>();
backTracking(nums,0);
return list;
}
public void backTracking(int[] nums,int startIndex){
if(path.size() >= 2){
list.add(new ArrayList<>(path));
}
//记录同一树层出现过的数字,如果已经出现过,置为1
//用set和map也可以,不过当元素较少时,使用数组比较快
//因为每当进入一个新树层时,都重新定义一个used数组,所以无需清除元素
//nums[i]的取值只有201中情况,所以长度定义为201
int[] used = new int[201];
for(int i = startIndex;i < nums.length;i ++){
//1.当当前元素小于path中的最后一个元素,注意不是当前元素的上一个元素,因为path中存储的数
//不一定在数组中是连续的
//2.或者说本树层中已经出现过当前元素了,为避免重复,停止此节点的递归
if(!path.isEmpty() && nums[i] < path.get(path.size() - 1) ||
(used[nums[i] + 100] == 1 )) continue;
path.add(nums[i]);
used[nums[i] + 100] = 1;
backTracking(nums,i + 1);
path.remove(path.size() - 1);
}
}
12.全排列(力扣46)
一个数组(不含重复元素),树枝去重
//2.回溯优化,used数组大小和nums.length相同即可
List<List<Integer>> list;
List<Integer> path;
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
list = new ArrayList<>();
path = new ArrayList<>();
used = new boolean[nums.length];
backTracking(nums);
return list;
}
public void backTracking(int[] nums){
if(path.size() == nums.length){
list.add(new ArrayList<>(path));
return;
}
for(int i = 0;i < nums.length;i ++){
if(used[i]) continue;
path.add(nums[i]);
used[i] = true;
backTracking(nums);
path.remove(path.size() - 1);
used[i] = false;
}
}
13.全排列 II(力扣47)
关于去重问题总结的最全的一个题
一个数组(有重复元素),树枝树层同时去重
//1.回溯
//如数组未排序
//在递归函数中for循环外定义used数组,进行树层去重
//将used数组定义为全局变量或是随递归函数传入,实现树枝相同去重
List<List<Integer>> list;
List<Integer> path;
boolean[] usedBranch;// 树枝相同去重使用
public List<List<Integer>> permuteUnique(int[] nums) {
list = new ArrayList<>();
path = new ArrayList<>();
usedBranch = new boolean[nums.length];
backTracking(nums);
return list;
}
public void backTracking(int[] nums){
if(path.size() == nums.length){
list.add(new ArrayList<>(path));
return;
}
//树层相同去重使用
int[] usedFloor = new int[21];
for(int i = 0;i < nums.length;i ++){
//树层和树枝相同去重
if(usedBranch[i] || (usedFloor[nums[i] + 10] == 1)) continue;
path.add(nums[i]);
usedBranch[i] = true;// 记录树枝出现过的数
usedFloor[nums[i] + 10] = 1; // 记录树层出现过的数
backTracking(nums);
path.remove(path.size() - 1);
usedBranch[i] = false;
}
}
//2.回溯
/*如果先对数组排序,可以利用另外一种方式
used数组定义为全局变量或是随着递归函数传入,那么去重可以有两种组合方式
1.树枝相同去重和树层去重
2.树枝相同去重和树枝不同去重
其实树枝不同去重的效果和树层去重是等效的,可以自己举(1,1)的例子
*/
List<List<Integer>> list;
List<Integer> path;
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
list = new ArrayList<>();
path = new ArrayList<>();
used = new boolean[nums.length];
//数组排序
Arrays.sort(nums);
backTracking(nums);
return list;
}
public void backTracking(int[] nums){
if(path.size() == nums.length){
list.add(new ArrayList<>(path));
return;
}
for(int i = 0;i < nums.length;i ++){
//树枝相同去重和树层去重
if(used[i] == true || (i > 0 && nums[i - 1] == nums[i] && !used[i - 1])){
continue;
}
//树枝相同去重和树枝不同去重,树枝不同的效果等效为树层去重
// if(used[i] == true || (i > 0 && nums[i - 1] == nums[i] && used[i - 1])){
// continue;
// }
path.add(nums[i]);
used[i] = true;
backTracking(nums);
path.remove(path.size() - 1);
used[i] = false;
}
}
14.重新安排行程(力扣332)
Deque<String> res;
Map<String, Map<String, Integer>> map;
public boolean backTracking(int ticketNum){
if(res.size() == ticketNum + 1){
return true;
}
String last = res.getLast();
if(map.containsKey(last)){//防止出现null
for(Map.Entry<String, Integer> target : map.get(last).entrySet()){
int count = target.getValue();
if(count > 0){
res.add(target.getKey());
target.setValue(count - 1);
if(backTracking(ticketNum)) return true;
res.removeLast();
target.setValue(count);
}
}
}
return false;
}
public List<String> findItinerary(List<List<String>> tickets) {
map = new HashMap<String, Map<String, Integer>>();
res = new LinkedList<>();
for(List<String> t : tickets){
Map<String, Integer> temp;
if(map.containsKey(t.get(0))){
temp = map.get(t.get(0));
temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1);
}else{
temp = new TreeMap<>();//升序Map
temp.put(t.get(1), 1);
}
map.put(t.get(0), temp);
}
res.add("JFK");
backTracking(tickets.size());
return new ArrayList<>(res);
}
15.N 皇后(力扣51)
List<List<String>> list;
public List<List<String>> solveNQueens(int n) {
list = new ArrayList<>();
char[][] chessBoard = new char[n][n];
for(char[] c : chessBoard){
Arrays.fill(c,'.');
}
backTracking(n,0,chessBoard);
return list;
}
//row:记录到了第几行
public void backTracking(int n,int row,char[][] chessBoard){
if(row == n){
list.add(array2List(chessBoard,n));
return;
}
for(int i = 0;i < n;i ++){
if(isValid(row,i,n,chessBoard)){
chessBoard[row][i] = 'Q';
backTracking(n,row + 1,chessBoard);
//回溯
chessBoard[row][i] = '.';
}
}
}
public List<String> array2List(char[][] chessBoard,int n){
List<String> res = new ArrayList<>();
for(char[] c : chessBoard){
StringBuilder sb = new StringBuilder();
for(char ch : c){
sb.append(ch);
}
res.add(sb.toString());
}
return res;
}
//判断当前(row,col)是否是有效的
//不在同一行,不在同一列,不在同一条斜线上
public boolean isValid(int row,int col,int n,char[][] chessBoard){
//不在同一列
for(int i = 0;i < row;i ++){
if(chessBoard[i][col] == 'Q'){
return false;
}
}
//45°角上的
for(int i = row - 1,j = col - 1;i >= 0 && j >= 0;i --,j--){
if(chessBoard[i][j] == 'Q'){
return false;
}
}
//135°角上的
for(int i = row - 1,j = col + 1;i >= 0 && j < n;i --,j ++){
if(chessBoard[i][j] == 'Q'){
return false;
}
}
return true;
}
//使用一维数组记录已取得位置;record[i] = j,第i行的第j列位置已经被占用
// public boolean isValid(int row,int col,int[] record){
// for(int i = 0;i < row;i ++){
// if(col == record[i] || Math.abs(row - i) == Math.abs(col - record[i])){
// return false;
// }
// }
// return true;
// }
16.解数独(力扣37)
//1.回溯,解释见:https://www.programmercarl.com/0037.%E8%A7%A3%E6%95%B0%E7%8B%AC.html#%E6%80%9D%E8%B7%AF
public void solveSudoku(char[][] board) {
backTracking(board);
}
public boolean backTracking(char[][] board){
for(int i = 0;i < 9;i ++){//遍历行
for(int j = 0;j < 9;j ++){//遍历列
if(board[i][j] != '.') continue;
for(char k = '1';k <= '9';k ++){//行列确定,从1-9遍历填入
if(isValid(i,j,k,board)){
board[i][j] = k;
if(backTracking(board)){
return true;
}
board[i][j] = '.';//回溯
}
}
//如果9个数都试过了,说明此路不通
return false;
}
}
//如果没有返回false,默认返回false
return true;
}
public boolean isValid(int row,int col,int number,char[][] board){
//判断同一行
for(int j = 0;j < 9;j ++){
if(board[row][j] == number){
return false;
}
}
// 判断同一列
for(int i = 0;i < 9;i ++){
if(board[i][col] == number){
return false;
}
}
// 判断同一格
int rowLeft = row / 3 * 3;
int colHigh = col / 3 * 3;
for(int i = rowLeft;i < rowLeft + 3;i ++){
for(int j = colHigh;j < colHigh + 3;j ++){
if(board[i][j] == number){
return false;
}
}
}
return true;
}