基本的套路
回溯法的解决的基本问题和下面的图中的内容基本一致:(均来源于代码随想录)层数的话一般是通过所需要的链表的长度有关(纵向的遍历),体现在终止条件那里,而横向的遍历是for循环那里代表了每一层的循环,即每一层是什么时候开始的,到什么时候结束,这是横向遍历,而纵向遍历的话,体现在backtracking,这里的话通过将每一层中的节点向下继续遍历而实现,相当于多个操作,回溯之后通常伴有撤销的操作,撤销的操作是必须的,可以通过这样的方式进行理解,第二层的子集合下有三个子集合,如下所示,进行操作是backtracking,意味着层数加一,而要遍历第二个子集合,就要先把第一个子集合的操作进行撤销,撤销后,从第二层再次开始,这个过程就是回溯。**下面展示一些 代码片
。
// An highlighted block
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
剪枝优化的过程
由于回溯的时间复杂度过高,所以通常需要进行剪枝的优化操作,而具体的过程如下图所示:
由上可见的是,每一层的终结的点是由当前所选择的数来决定的,当前所选择的数为0时,如果是n=4,k=3的情形,当前已经选择的元素为0时,即第一层的元素,那么pathsize=0,k-pathsize=3,则当前终结的点是n-(k-pathsize)+1,即终结于2这个点,那么集合{2,3,4}也是合理的。
组合问题的求解
// An highlighted block
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
组合的和为定值(组合中元素的个数一定)
一共有几个地方可以剪枝吧,就是在sum> targetsum的时候可以剪枝,然后就是横向剪枝
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
//组合的综合为定值且组合中元素的个数是一定的
//一共有几个地方可以剪枝吧,就是在sum> targetsum的时候可以剪枝,然后就是横向剪枝
public class backtrack1 {
static LinkedList<Integer> path=new LinkedList<>();
static List<List<Integer>> result=new ArrayList<>();
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int k=sc.nextInt();
int n=sc.nextInt();
System.out.println(combinationSum3(k,n));
}
public static List<List<Integer>> combinationSum3(int k, int n) {
backtrack(k,0,n,1);
return result;
}
public static void backtrack(int k, int sum, int target, int startIndex){
if(sum>target){
return;
}
if(path.size()==k && sum==target){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= 9 -(k-path.size())+1; i++) {
path.add(i);
sum+=i;
backtrack(k,sum,target,i+1);
sum-=i;
path.removeLast();
}
}
}
组合总和(组合的元素可以重复)
给定一个无重复元素的正整数数组 candidates 和一个正整数 target ,找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的唯一组合数少于 150 个。
输入: candidates = [2,3,6,7], target = 7
输出: [[7],[2,2,3]]
List<List<Integer>> res=new ArrayList<>();
LinkedList<Integer> temp=new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(candidates,target,0);
return res;
}
public void dfs(int[] candidates, int target,int Index){//target表示还剩下的数字
if(target==0){
res.add(new ArrayList<>(temp));
return;
}
if(Index == candidates.length){
return; //表示搜索不到的时候也直接返回
}
dfs(candidates,target,Index+1); //跳过
if(target-candidates[Index]>=0){ //可以选择
temp.add(candidates[Index]);
dfs(candidates,target-candidates[Index],Index);
temp.removeLast();
}
}
这里的思路就是先进行选择,可以选择跳过,也可以选择不跳过
一开始可以先直接跳过最后到最后一位的时候开始返回,此时到上一层开始选择不跳过,于是选择了7,满足条件,再次返回。。。。以此类推
八皇后问题
一个经典的回溯算法。皇后同行,同列和对角线均不能有皇后。
思路就是第一行第一列中先设置一个Q,然后是判断回溯过程是否有效,里面的话包含的是设置棋盘中此时的位置为Queen,然后在到下一层的backtrack,由于到下一层以后,再进行回溯的时候需要对下一层的操作进行撤销,即回到上一层。最后的终止条件为到了最后一行,同时将棋盘中的元素进行输出,得到的最终的结果。
在这里插入代码片
import java.util.*;
public class Queen {
static List<List<String>> res=new LinkedList<>();
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int m = sc.nextInt();
char[][] chess=new char[m][m];
for (int i = 0; i < m; i++) {
for (int j = 0; j < m; j++) {
chess[i][j]='.';
}
}
backTrack(m,0,chess);
System.out.println(res);
}
public static void backTrack(int n, int row, char[][] chessboard) {
if (row == n) {
res.add(Array2List(chessboard));
return;
}
for (int col = 0;col < n; col++) {
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTrack(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
public static List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public static 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-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
解数独
由于此处保证的是输入的数独只有一个解法,因而可以进行回溯求解,在回溯的过程判断当前的数独是否为和要求的数独。这个问题是很特殊的,以往的回溯中,通常是每一层都只能放一个数,然后进行回溯的递归,但是这里解数独不一样的是,这里应该每一行和每一列都进行填充,因而需要使用两重for循环进行回溯。
在这里插入代码片
public class sudu {
public static void main(String[] args) {
char[][] board = new char[][]{
{'5', '3', '.', '.', '7', '.', '.', '.', '.'},
{'6', '.', '.', '1', '9', '5', '.', '.', '.'},
{'.', '9', '8', '.', '.', '.', '.', '6', '.'},
{'8', '.', '.', '.', '6', '.', '.', '.', '3'},
{'4', '.', '.', '8', '.', '3', '.', '.', '1'},
{'7', '.', '.', '.', '2', '.', '.', '.', '6'},
{'.', '6', '.', '.', '.', '.', '2', '8', '.'},
{'.', '.', '.', '4', '1', '9', '.', '.', '5'},
{'.', '.', '.', '.', '8', '.', '.', '7', '9'}
};
solveSudoku(board);
printBoard(board);
}
private static void printBoard(char[][] board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
public static void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private static boolean solveSudokuHelper(char[][] board){
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
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++){ // (i, j) 这个位置放k是否合适
if (isValidSudoku(i, j, k, board)){
board[i][j] = k;
if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
return true;
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private static boolean isValidSudoku(int row, int col, char val, char[][] board){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
}
递归与回溯的关系
首先递归中就必然包含着回溯的过程
以利扣孙笑川大佬的图为例,递归在调用进行返回的时候,就会产生回溯的过程,但具体上需不需要进行额外的回溯操作需要视情况而定,以一道题为例:
257. 二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
示例: 257.二叉树的所有路径1
在这里插入图片描述
最后的输出应该是[“643”,“641”,“678”],可以看出的是使用前序遍历进行遍历的,就是一路进行递归:
而这里不仅递归中有回溯回原点的过程,回溯本身也有操作,即删除下一个节点的值,回溯到root.val的过程,也就是说回溯的过程是为了防止重复的添加了节点的值。
// 递归和回溯是同时进行,所以要放在同一个花括号里
if (root.left != null) { // 左
traversal(root.left, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
if (root.right != null) { // 右
traversal(root.right, paths, res);
paths.remove(paths.size() - 1);// 回溯
}
就是说当你已经遇到了节点的分支的时候就要用回溯去删掉一些东西
当遇到6->4->3的时候,3需要回溯,但在回溯到4的右节点之前,先把路径的值给弄出来,再回溯到4的右节点,然后同样的操作了
在这里插入代码片
public class erchashuSum {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
private TreeNode root;
TreeNode() {
}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public static void main(String[] args) {
TreeNode root=new TreeNode(1);
TreeNode node1=new TreeNode(2);
TreeNode node3=new TreeNode(3);
TreeNode node4=new TreeNode(4);
root.right=node1;
node1.right=node3;
node3.right=node4;
System.out.println(binaryTreePaths(root));
}
public static List<String> binaryTreePaths(TreeNode root) {
List<Integer> path=new ArrayList<>();
List<String> res=new ArrayList<>();
if(root==null){
return res;
}
dfs(root,path,res);
return res;
}
public static void dfs(TreeNode root,List<Integer> path,List<String> res){
path.add(root.val); //前序遍历
if(root.left==null && root.right==null){
StringBuffer sb=new StringBuffer();
for (int i = 0; i < path.size()-1; i++) {
sb.append(path.get(i)).append("->");
}
//加上最后的一个节点值
sb.append(path.get(path.size()-1));
res.add(sb.toString());
return;
}
if (root.left!=null){
dfs(root.left,path,res);
path.remove(path.size()-1);
}
if(root.right!=null){
dfs(root.right,path,res);
path.remove(path.size()-1);
}
}
}
计算二叉树的最大深度也可以使用递归加上回溯的思路:
每次递归的时候,depth+1,并更新maxdepth的值,最后回溯回上一个节点。
在这里插入代码片
public class maxDepth {
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
private TreeNode root;
TreeNode() {
}
TreeNode(int val) {
this.val = val;
}
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
int res=1;
public static void main(String[] args) {
TreeNode root=new TreeNode(1);
TreeNode node1=new TreeNode(2);
TreeNode node3=new TreeNode(3);
TreeNode node4=new TreeNode(4);
root.right=node1;
node1.right=node3;
node3.right=node4;
maxDepth maxDepth=new maxDepth();
System.out.println(maxDepth.maxDepth(root));
}
public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
dfs(root,1);
return res;
}
public void dfs(TreeNode root,int dep){
res= Math.max(res,dep);
if(root.left==null &&root.right==null){
return ;
}
if(root.left!=null){
dep++;
dfs(root.left,dep);
dep--;
}
if(root.right!=null){
dep++;
dfs(root.right,dep);
dep--;
}
}
}
在这里插入代码片
相信通过这个例子可以对dfs中的递归和回溯的过程更加深入
回溯隐含在参数中
值得注意的是有些回溯没有明确的给出,但是实际上也是回溯,只是在参数中隐含了,例如题目二叉树的所有路径。
正整数 n 代表生成括号的对数,请设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
这道题也一样,在题目中,可以使用两种回溯的方式:
class Solution {
List<String> list=new ArrayList<>();
public List<String> generateParenthesis(int n) {
generate(n,n,"");
return list;
}
public void generate(int left, int right, String cur){
if(left==0&&right==0){
list.add(cur);
return;
}
if(left==right){
generate(left-1,right,cur+"(");
}else if(left<right){
if(left>0){
generate(left-1,right,cur+"(");
}
generate(left,right-1,cur+")");
}
}
}
一种方法是上面的方法,generate(left-1,right,cur+"(");实际上包含了回溯的过程,因为参数cur的值
在传参的时候实际上没有改变
class Solution {
public List<String> generateParenthesis(int n) {
List<String> list=new ArrayList<>();
generate(0,0,n,new StringBuffer(),list);
return list;
}
public void generate(int left,int right,int max,StringBuffer stringBuffer,List<String>list) {
if(stringBuffer.length()==2*max){
list.add(stringBuffer.toString());
return;
}
if(left<max){
stringBuffer.append('(');
generate(left+1,right,max,stringBuffer,list);
stringBuffer.deleteCharAt(stringBuffer.length()-1);
}
if(right<left){
stringBuffer.append(')');
generate(left,right+1,max,stringBuffer,list);
stringBuffer.deleteCharAt(stringBuffer.length()-1);
}
}
}
这也是回溯的一种方式,在这里的话,实际上是已经改变了stringbuffer的值了,stringBuffer.append(')');
因此需要删掉最后一位的元素。