递归回溯算法习题解析

“种一棵树最好的时间是十年前,其次是现在”

回溯题是笔试面试中经常考察的内容,如求一系列的组合问题,以及在一定规则下搜索出满足条件的集合。

今天刷了一些回溯题,特此总结一下。

先看一道组合题:

电话键的组合

输入一个字符串,返回其所有的可能组合类型

输入:“23”

输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”] 不分顺序
在这里插入图片描述

不多说,直接手撸代码

//先构建哈希表
HashMap<String,String> 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");
}};
//返回结果
List<String> res = new LinkedList<>();
//主函数
public List<String> letterCombine(String digits){
    if(digits.length()!=0){
        backTrack("",digits);
    }
    return res;
}
//回溯方法
public void backTrack(String cur,String restDig){
    if(restDig.length()==0){
        res.add(cur);
    }else{
        String digit = restDig.subString(0,1);
        String letters = map.get(digit);
        for(int i=0;i<letters.length();i++){
            String letter = letters.subString(i,i+1);
            backTrack(cur+letter,restDig.subString(1));
        }
    }
}

在回溯体中,我们一次构建了:结束条件、中间体(选择判断)、下一步入口,后面的问题中可能还涉及到回溯过程的修改

接着看下一题:

括号生成

数字n代表括号的对数,设计一个函数生成所有可能的有效的括号组合

输入:3

输出:"((()))","(()())","(())()","()(())","()()()"

这次我们该如何组合呢,先尝试用回溯

public List<String> func(int n){
    List<String> res = new LinkedList<>();
    if(n==0){
        return res;
    }
    dfs(new StringBuilder(),n,n,res);
    return res;
}
public void dfs(StringBuilder cur,int left,int right,List<String> res){
    if(left==0 && right==0){
        res.add(cur.toString());
        return;
    }
    //剪枝
    if(left>right){
        return;
    }
    if(left>0){
        dfs(cur.append("("),left-1,right,res);
    }
    if(right>0){
        dfs(cur.append(")"),left,right-1,res);
    }
}

分析一下做法:最后的字符串的基本要求是左括号数=右括号数

结束条件:左右括号数用完

剪枝:左>右,错误格式

回溯题型

类型题目链接
子集、组合子集子集2组合组合总和组合总和2
全排列全排列全排列2字符串的全排列字母大小写全排列
搜索解数独单词搜索N皇后分割回文串二进制手表

回溯6步走

  • ①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
    ②根据题意,确立结束条件
    ③找准选择列表(与函数参数相关),与第一步紧密关联※
    ④判断是否需要剪枝
    ⑤作出选择,递归调用,进入下一层
    ⑥撤销选择

子集

输入一组不含重复数字的数组,求出其所有的子集

img

简洁版:

class Solution{
    public List<List<Integer>> subsets(int[] nums){
        List<List<Integer>> res = new ArrayList<>();
        backtrack(0,nums,res,new ArrayList<Integer>());
        return res;
    }
    //start,nums,res,temp
    public void backtrack(int start,int[] nums,List<List<Integer>> res,List<Integer> temp){
        res.add(new ArrayList(temp));
       	for(int i=start;i<nums.length;i++){
            temp.add(nums[i]);
            backtrack(i+1,nums,res,temp);
            temp.remove(temp.size()-1);
        }
    }
}
子集2

img

class Solution{
    public List<List<Integer>> subsets(int[] nums){
        Arrays.sort(nums);
        List<List<Integer>> res = new ArrayList<>();
        backtrack(0,nums,res,new ArrayList<Integer>());
        return res;
    }
    public void backtrack(int start,int[] nums,List<List<Integer>> res,List<Integer> temp){
        res.add(new ArrayList(temp));
        for(int i=start;i<nums.length;i++){
            //发现这个数和前面的数相同,剪枝
            if(i>start && nums[i]==nums[i-1]){
                continue;
            }
            temp.add(nums[i]);
            backtrack(i+1,nums,res,temp);
            temp.remove(temp.size()-1);
        }
    }
}
组合

给定整数n和k,返回1…n中所有可能的k个数的组合

输入:n=4,k=2

输出:【1,2】,【1,3】,【1,4】,【2,3】,【2,4】,【3,4】

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    
    public List<List<Integer>> combine(int n, int k) {
		backtrack(1,n,k,new LinkedList<Integer>());
        return res;
    }
    public void backtrack(int start,int n,int k,List<Integer> temp){
        if(temp.size()==k){
            res.add(new LinkedList(temp));
            return;
        }
        for(int i=start;i<=n-(k-temp.size())+1;i++){
            temp.add(i);
            backtrack(i+1,n,k,temp);
            temp.remove(temp.size()-1);
        }
    }
}
组合总和

给定一个无重复数组nums[]和一个目标数target,从数组中找到使数字和为target的组合

数字可以被多次重复选取

输入:【1,2,3】 target=3

输出:【1,1,1】,【1,2】,【3】

img

class Solution{
    List<List<Integer>> res = new ArrayList<>();
    
    public List<List<Integer>> combinationSum(int[] nums,int target){
        backtrack(0,target,nums,new ArrayList<Integer>());
        return res;
    }
    
    public void backtrack(int start,int sum,int[] nums,List<Integer> temp){
        if(sum==0){
            res.add(new ArrayList<>(temp));
            return;
        }
        for(int i=start;i<nums.length;i++){
            if(sum-nums[i]<0){
                continue;
            }
            temp.add(nums[i]);
            backtrack(i,sum-nums[i],nums,temp);
            temp.remove(temp.size()-1);
        }
    }
}
组合总和2

给定一个无重复数组nums[]和一个目标数target,从数组中找到使数字和为target的组合

数字仅能被选择1次

输入:【1,2,3】 target=3

输出:【1,1,1】,【1,2】,【3】

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtrack(0,candidates,target,new LinkedList<Integer>());
        return res;
    }
    public void backtrack(int start,int[] nums,int target,LinkedList<Integer> temp){
        if(target==0){
            res.add(new LinkedList(temp));
            return;
        }
        for(int i=start;i<nums.length;i++){
            if(target-nums[i]<0){
                break;
            }
            if(i>start && nums[i]==nums[i-1]){
                continue;
            }
            temp.add(nums[i]);
            backtrack(i+1,nums,target-nums[i],temp);
            temp.remove(temp.size()-1);
        }
    }
}

全排列

给定一个没有重复数字的序列,返回其所有可能的全排列(最好按字典序)

输入:【1,2,3】

输出:【1,2,3,】,【1,3,2】,【2,1,3】,【2,3,1】,【3,1,2】,【3,2,1】

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        boolean[] visited = new boolean[nums.length];
        backtrack(nums,visited,new LinkedList<Integer>());
        return res;
    }
    public void backtrack(int[] nums,boolean[] visited,LinkedList<Integer> temp){
        if(temp.size()==nums.length){
            res.add(new LinkedList(temp));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(!visited[i]){
                visited[i] = true;
                temp.add(nums[i]);
                backtrack(nums,visited,temp);
                temp.remove(temp.size()-1);
                visited[i] = false;
            }
        }
    }
}
全排列2

给定一个可能有重复数字的序列,返回不重复的全排列(最好按字典序)

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        boolean[] flag = new boolean[nums.length];
        Arrays.sort(nums);
        backtrack(nums,flag,new LinkedList<Integer>());
        return res;
    }
    public void backtrack(int[] nums,boolean[] flag,LinkedList<Integer> temp){
        if(temp.size()==nums.length){
            res.add(new LinkedList(temp));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(!flag[i]){
                //去重剪枝,注意这里!flag[i-1]指上一个数没用过
                //如果flag[i-1]=true说明上个数在使用中,不能剪枝,如【1,1,2】
                if(i>0 && nums[i]==nums[i-1] && !flag[i-1]){
                    continue;
                }
                flag[i] = true;
                temp.add(nums[i]);
                backtrack(nums,flag,temp);
                flag[i] = false;
                temp.remove(temp.size()-1);
            }
        }
    }
}
字符串的排列

输入个字符串,打印出该字符串中字符的所有排列(无重复)

输入: s = “abc”

输出:【 “abc”,“acb”,“bac”,“bca”,“cab”,“cba” 】

关键点:先对字符数组进行排序,进行去重剪枝,arr[i]==arr[i-1] && !used[i]

这里

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        boolean[] flag = new boolean[nums.length];
        Arrays.sort(nums);
        backtrack(nums,flag,new LinkedList<Integer>());
        return res;
    }
    public void backtrack(int[] nums,boolean[] flag,LinkedList<Integer> temp){
        if(temp.size()==nums.length){
            res.add(new LinkedList(temp));
            return;
        }
        for(int i=0;i<nums.length;i++){
            if(!flag[i]){
                //去重剪枝,注意这里!flag[i-1]指上一个数没用过
                if(i>0 && nums[i]==nums[i-1] && !flag[i-1]){
                    continue;
                }
                flag[i] = true;
                temp.add(nums[i]);
                backtrack(nums,flag,temp);
                flag[i] = false;
                temp.remove(temp.size()-1);
            }
        }
    }
}
字母大小写全排列

给定一个字符串S,通过这个字符串S中的每个字母转为大小写,可以获得一个新的字符串,返回所有可能的字符串集合。

输入:S = “a1b2”

输出:【a1b2】,【a1B2】,【A1b2】,【A1B2】

class Solution3 {
    List<String> res = new LinkedList<>();
    public List<String> letterCasePermutation(String S) {
        char[] arr = S.toCharArray();
        backtrack(arr,0);
        return res;
    }
    public void backtrack(char[] arr,int start){
        if(start==arr.length){
            res.add(new String(arr));
            return;
        }
        backtrack(arr,start+1);
        //回溯部分
        if(arr[start]>='A'){
            arr[start] = arr[start] < 'a' ? (char)(arr[start]+32):(char)(arr[start]-32);
            backtrack(arr,start+1);
        }
    }
}

搜索

搜索其实就是对集合/排列的一种包装,仍然以回溯算法为核心

二进制的组合(手表灯)
class Solution4 {
    List<String> res = new LinkedList<>();

    public List<String> readBinaryWatch(int num) {
        backtrack(num, 0, 0, 0);
        return res;
    }

    public void backtrack(int num, int start, int hour, int minute) {
        if (hour > 11 || minute > 59) {
            return;
        }
        if (num == 0) {//使用完亮灯数,进行添加
            String minStr = String.valueOf(minute);
            if (minute < 10) {
                minStr = "0" + minStr;
            }
            res.add(hour + ":" + minStr);
            return;
        }
        for (int i = start; i < 10; i++) {
            if (i < 4) {
                hour += (1 << i);
            } else {
                minute += (1 << (i-4));
            }
            backtrack(num - 1, i + 1, hour, minute);
            if (i < 4) {
                hour -= (1 << i);
            } else {
                minute -= (1 << (i-4));
            }
        }
    }
}
解数独

根据一张数独表填充剩下的数字,规则如下:

1.数字1-9在每一行只能出现1次

2.数字1-9在每一列只能出现1次

3.数字1-9在表内的3×3宫内只能出现1次

假设数独只有唯一解

尝试深度遍历进行填充,如果重复就擦除重新填写

class Solution6 {
    public void solveSudoku(char[][] board) {
        //记录每行的1-9数字是否使用,false为使用过
        boolean[][] rowUsed = new boolean[9][10];
        //记录每列的1-9数字是否使用
        boolean[][] colUsed = new boolean[9][10];
        //记录每个宫格的1-9数字是否使用
        boolean[][][] boxUsed = new boolean[3][3][10];
        //已有数字初始化
        for(int i=0;i<board.length;i++){
            for(int j=0;j<board[0].length;j++){
                int num = board[i][j] - '0';
                if(num>0 && num <=9){
                    rowUsed[i][num] = true;
                    colUsed[j][num] = true;
                    boxUsed[i/3][j/3][num] = true;
                }
            }
        }
        //进入回溯
        backtrack(board,rowUsed,colUsed,boxUsed,0,0);
    }
    public boolean backtrack(char[][] board,boolean[][] rowUsed,boolean[][] colUsed,boolean[][][] boxUsed,int row,int col){
        //结束条件
        if(col==board[0].length){
            col = 0;//重新回到第一列
            row++;
            if(row==board.length){
                return true;
            }
        }
        //如果未填写,选择填写该空
        if(board[row][col]=='.'){
            for(int i=1;i<=9;i++){
                boolean canUse = !(rowUsed[row][i] ||
                        colUsed[col][i] ||
                        boxUsed[row/3][col/3][i]);
                if(canUse){
                    rowUsed[row][i] = true;
                    colUsed[col][i] = true;
                    boxUsed[row/3][col/3][i] = true;
                    board[row][col] = (char) (i + '0');
                    if(backtrack(board,rowUsed,colUsed,boxUsed,row,col+1)){
                        return true;
                    }
                    //进入回溯撤销阶段:
                    board[row][col] = '.';
                    rowUsed[row][i] = false;
                    colUsed[col][i] = false;
                    boxUsed[row/3][col/3][i] = false;
                }
            }
        }else{	//执行下一步
            return backtrack(board,rowUsed,colUsed,boxUsed,row,col+1);
        }
        return false;
    }
}
N皇后

打印N皇后在N×N棋盘上的各种摆法,每个皇后不能同行或同列,也不在对角方向,同样需要用到回溯搜索

本题重点:如何判断斜线方向是否满足—>找到漂移量

class Solution7 {
    List<List<String>> res = new LinkedList<>();
    public List<List<String>> solveNQueens(int n) {
        //棋盘初始化
        char[][] board= new char[n][n];
        for(int i=0;i<n;i++){
            Arrays.fill(board[i], '.');
        }
        backtrack(board,0);
        return res;
    }
    public void backtrack(char[][] board,int row){
        int len = board.length;
        if(row==len){
            List<String> temp = new LinkedList<>();
            for(int i=0;i<len;i++){
                temp.add(String.valueOf(board[i]));
            }
            res.add(temp);
            return;
        }
        for(int col = 0;col<len;col++){
            //判断这个位置是否合适
            boolean proper = true;
            for(int i=0;i<row;i++){
                //检查列方向
                if(board[i][col]=='Q'){
                    proper = false;
                    break;
                }
                //检查右上
                if(col+(row-i)<len && board[i][col+(row-i)]=='Q'){
                    proper = false;
                    break;
                }
                //检查右下
                if(col-(row-i)>=0 && board[i][col-(row-i)]=='Q'){
                    proper = false;
                    break;
                }
            }
            //不满足条件,跳过此处
            if(!proper){
                continue;
            }
            board[row][col] = 'Q';
            backtrack(board,row+1);
            //回溯撤销
            board[row][col] = '.';
        }
    }
}
单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中,其中单词可以通过水平或垂直的相邻网格构成,同一单元格的字母不允许被重复使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1huc6K1A-1596459076022)(C:\Users\10764\AppData\Roaming\Typora\typora-user-images\image-20200803192346547.png)]

class Solution8 {
    boolean[][] used;
    int[][] dir = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    int row, col;
    char[][] board;
    String word;

    public boolean exist(char[][] board, String word) {
        row = board.length;
        col = board[0].length;
        used = new boolean[row][col];
        this.board = board;
        this.word = word;

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (backtrack(i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean backtrack(int i, int j, int start) {
        //start步数
        if (start == word.length() - 1) {
            return board[i][j] == word.charAt(start);
        }
        //判断
        if (board[i][j] == word.charAt(start)) {
            used[i][j] = true;
            for (int k = 0; k < 4; k++) {
                int nextRow = i + dir[k][0];
                int nextCol = j + dir[k][1];
                if(isIn(nextRow,nextCol) && !used[nextRow][nextCol]){
                    if(backtrack(nextRow,nextCol,start+1)){
                        return true;
                    }
                }
            }
            used[i][j] = false;
        }
        return false;
    }
    public boolean isIn(int x,int y){
        return (x>=0 && x < row && y>=0 && y<col);
    }
}
分割回文串

给定一个字符串S,将S分割成一些子串,使每个子串都是回文串,输出所有方案

输入:“aab”

输出:[ [“aa”,“b”],[ “a”,“a” ,“b”] ]

class Solution9 {
    List<List<String>> res = new LinkedList<>();
    public List<List<String>> partition(String s) {
        backtrack(s,0,s.length(),new LinkedList<>());
        return res;
    }

    public void backtrack(String s,int start,int len,List<String> temp){
        if(start==len){
            res.add(new LinkedList<>(temp));
            return;
        }
        for(int i=start;i<len;i++){
            //截取字符串很消耗时间
            if(!isHuiWen(s,start,i)){
                continue;
            }
            temp.add(s.substring(start,i+1));
            backtrack(s,i+1,len,temp);
            temp.remove(temp.size()-1);
        }
    }

    public boolean isHuiWen(String s,int left,int right){
        while(left<right){
            if(s.charAt(left)!=s.charAt(right)){
                return false;
            }
            left++;
            right--;
        }
        return true;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值