回溯算法专题

目录

🌞回溯算法概念

🌻算法思想

🌂深度优先搜索例题

💧全排列问题

☔全排列

☔字符串的排列

☔子集

☔括号生成

☔电话号码的字母组合

☔组合总和

☔活字印刷

💧树形结构问题

☔员工的重要性

☔二叉树中和为某一值的路径

💧二维矩阵问题

☔图像渲染

☔岛屿的周长

☔被围绕的区域

☔岛屿数量

☔岛屿的最大面积

☔N皇后

☔单词搜索

🌂广度优先搜索

💧转盘锁问题

☔打开转盘锁

☔单词接龙

☔最小基因变化

 💧树形结构问题

☔N叉树的层序遍历

💧二维矩阵问题

☔腐烂的橘子

😀总结

🌈深度优先算法模板

🌈广度优先算法模板


相关算法专题

动态规划专题

贪心算法专题

🌞回溯算法概念

🌻算法思想

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。

除过深度优先搜索,常用的还有广度优先搜索。

🌂深度优先搜索例题

💧全排列问题

☔全排列

题目:

        给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例:

        输入:nums = [1,2,3]

        输出:[ [1,2,3] , [1,3,2] , [2,1,3] , [2,3,1] , [3,1,2] , [3,2,1] ]

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        List<Integer> output = new ArrayList<Integer>();
        //先把一种方法写进去
        for (int num : nums) {
            output.add(num);
        }
        int n = nums.length;
        backtrack(n, output, res, 0);
        return res;
    }
    public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
        // 所有数都填完了
        if (first == n) {
            res.add(new ArrayList<Integer>(output));
        }
        for (int i = first; i < n; i++) {
            // 动态维护数组
            Collections.swap(output, first, i);
            // 继续递归填下一个数
            backtrack(n, output, res, first + 1);
            // 撤销操作
            Collections.swap(output, first, i);
        }
    }
}

☔字符串的排列

题目:

        输入一个字符串,打印出该字符串中字符的所有排列。

        你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

        输入:s = "abc"

        输出:[ "abc","acb","bac","bca","cab","cba" ]

class Solution {
    public String[] permutation(String s) {
        HashSet<String> set= new HashSet<>();
        List<String> ans = new ArrayList<>();
        boolean[] flag = new boolean[s.length()];
        dfs(new StringBuffer(),s,0,ans,flag,set);
        return ans.toArray(new String[ans.size()]);

    }
    public void dfs(StringBuffer str,String s,int i,List<String> ans,boolean[] flag,HashSet<String> set){
        if(s.length()==i){
            if(set.contains(str.toString())){
                return;
            }
            ans.add(str.toString());
            //按照题目要求去重
            set.add(str.toString());
            return;
        }
        for(int j=0;j<s.length();++j){
            if(flag[j]==true){
                continue;
            }
            str.append(s.charAt(j));
            //做一个标记,用过的字母就不要再用了
            flag[j]=true;
            dfs(str,s,i+1,ans,flag,set);
            flag[j]=false;
            str.deleteCharAt(str.length()-1);
        }
    }
}

☔子集

题目:

        给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

        解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例:

        输入:nums = [1,2,3]

        输出:[ [ ] ,[1] , [2] , [1,2] , [3] , [1,3] , [2,3] , [1,2,3] ]

class Solution {
    List<List<Integer>> ans;
    public List<List<Integer>> subsets(int[] nums) {
        ans = new ArrayList<>();
        List<Integer> list = new ArrayList<>();
        dfs(0,nums,list);
        return ans;
    }
    public void dfs(int i,int[] nums,List<Integer> list){
        //如果递归完成,就入链表
        if(i==nums.length){
            ans.add(new ArrayList<>(list));
            return;
        }
        list.add(nums[i]);
        //加当前元素的情况
        dfs(i+1,nums,list);
        list.remove(list.size()-1);
        //不加当前元素的情况
        dfs(i+1,nums,list);
    }
}

☔括号生成

题目:

        数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例:

        输入:n = 3

        输出:[ "( ( ( ) ) ) " , "( ( ) ( ) ) " , "( ( ) ) ( )" , "( ) ( ( ) )" , "( ) ( ) ( )" ]

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> list = new ArrayList<String>();
        backtrack(list, new StringBuilder(), 0, 0, n);
        return list;
    }

    public void backtrack(List<String> list, StringBuilder s, int left, int right, int n) {
        if(n*2==s.length()){
            list.add(s.toString());
            return;
        }
        //左边未匹配括号数量小于n,进行加左括号的回溯操作
        if(left<n){
            s.append('(');
            backtrack(list,s,left+1,right,n);
            s.deleteCharAt(s.length()-1);
        }
        //同理
        if(right<left){
            s.append(')');
            backtrack(list,s,left,right+1,n);
            s.deleteCharAt(s.length()-1);
        }
    }
}

☔电话号码的字母组合

题目:

        给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

        给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例:

 

        输入:digits = "23"

        输出:[ "ad","ae","af","bd","be","bf","cd","ce","cf" ]

class Solution {
    public List<String> letterCombinations(String digits) {
        List<String> list = new ArrayList<String>();
        if (digits.length() == 0) {
            return list;
        }
        Map<Character, String> map = new HashMap<Character, String>() {{
            put('2', "abc");
            put('3', "def");
            put('4', "ghi");
            put('5', "jkl");
            put('6', "mno");
            put('7', "pqrs");
            put('8', "tuv");
            put('9', "wxyz");
        }};
        dfs(list, map, digits, 0, new StringBuffer());
        return list;
    }
    public void dfs(List<String> list, Map<Character, String> map, String digits, int i, StringBuffer s) {
        //递归完成就加入链表
        if(i==digits.length()){
            list.add(s.toString());
        }else{
            //套用回溯模板
            String str = map.get(digits.charAt(i));
            for(int j=0;j<str.length();++j){
                s.append(str.charAt(j));
                dfs(list,map,digits,i+1,s);
                s.deleteCharAt(i);
            }
        }
    }
}

☔组合总和

题目:

        给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

        candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

        对于给定的输入,保证和为 target 的不同组合数少于 150 个。 

示例:

        输入:candidates = [2,3,6,7], target = 7

        输出:[ [ 2,2,3 ] , [ 7 ] ]

解释:

        2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。

        7 也是一个候选, 7 = 7 。

        仅有这两种组合。

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        List<Integer> list = new ArrayList<Integer>();
        dfs(candidates, target, ans, list, 0);
        return ans;
    }

    public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> list, int i) {
        if (i == candidates.length) {
            return;
        }
        if (target == 0) {
            ans.add(new ArrayList<Integer>(list));
            return;
        }
        //只要西澳娱目标值,就进行回溯操作
        if (target - candidates[i] >= 0) {
            list.add(candidates[i]);
            dfs(candidates, target - candidates[i], ans, list, i);
            list.remove(list.size() - 1);
        }
        //下一个元素的递归
        dfs(candidates, target, ans, list, i + 1);
    }
}

☔活字印刷

题目:

        你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。

注意:

        本题中,每个活字字模只能使用一次。

示例:

        输入:"AAB"

        输出:8

 解释:

        可能的序列为 "A", "B", "AA", "AB", "BA", "AAB", "ABA", "BAA"。

class Solution {
    public int numTilePossibilities(String tiles) {
        boolean[] visited = new boolean[tiles.length()];
        //引入哈希集合去重
        Set<String> set = new HashSet<>();
        dfs(visited, set, tiles, new StringBuilder());
        return set.size() - 1;
    }
    public void dfs(boolean[] visited, Set<String> set, String tiles, StringBuilder sb) {
        set.add(sb.toString());
        for (int i = 0; i < tiles.length(); i++) {
            if (!visited[i]) {
                //做标记,如果用过这个字母,就不进行此次操作
                visited[i] = true;
                sb.append(tiles.charAt(i));
                dfs(visited, set, tiles, sb);
                sb.deleteCharAt(sb.length() - 1);
                visited[i] = false;
            }
        }
    }
}

💧树形结构问题

☔员工的重要性

题目:

        给定一个保存员工信息的数据结构,它包含了员工 唯一的 id ,重要度 和 直系下属的 id 。

        比如,员工 1 是员工 2 的领导,员工 2 是员工 3 的领导。他们相应的重要度为 15 , 10 , 5 。那么员工 1 的数据结构是 [1, 15, [2]] ,员工 2的 数据结构是 [2, 10, [3]] ,员工 3 的数据结构是 [3, 5, []] 。注意虽然员工 3 也是员工 1 的一个下属,但是由于 并不是直系 下属,因此没有体现在员工 1 的数据结构中。

        现在输入一个公司的所有员工信息,以及单个员工 id ,返回这个员工和他所有下属的重要度之和。 

示例:

        输入:[  [1, 5, [2, 3] ], [2, 3, [ ] ], [3, 3, [ ] ]  ], 1

        输出:11

解释:

        员工 1 自身的重要度是 5 ,他有两个直系下属 2 和 3 ,而且 2 和 3 的重要度均为 3 。因此员工 1 的总重要度是 5 + 3 + 3 = 11 。 

提示:

        一个员工最多有一个 直系 领导,但是可以有多个 直系 下属
 

/*
// Definition for Employee.
class Employee {
    public int id;
    public int importance;
    public List<Integer> subordinates;
};
*/

class Solution {
    public int dfs(Map<Integer,Employee>info,int id){
        //下属为空是边界,下属为空时,return的是0
        Employee curE = info.get(id);
        int sum = curE.importance;
        for(int subId : curE.subordinates){
            //每次先加第一个下属的重要性
            //按照相同的操作再去加下属的第一个下属的重要性
            sum+=dfs(info,subId);
        }
        return sum;
    }
    public int getImportance(List<Employee> employees, int id) {
        if(employees.isEmpty())
            return 0;
        Map<Integer,Employee> info = new HashMap<>();
        for(Employee e:employees){
            info.put(e.id,e);
        }
        //从第一个员工开始
        return dfs(info,id);
    }
}

☔二叉树中和为某一值的路径

题目:

        给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

        叶子节点 是指没有子节点的节点。

示例:

        输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22

        输出:[ [5,4,11,2] , [5,8,4,5] ]

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    List<List<Integer>> list = new LinkedList<>();
    Deque<Integer> queue = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        dfs(root,target);
        return list;
    }
    public void dfs(TreeNode root , int target){
        if(root==null){
            return;
        }
        queue.offerLast(root.val);
        target-=root.val;
        //符合元素的加入到链表
        if(root.left==null&&root.right==null&&target==0){
            list.add(new LinkedList<>(queue));
        }
        //递归进行左右子树的查询
        dfs(root.left,target);
        dfs(root.right,target);
        //回溯
        queue.pollLast();
    }
}

💧二维矩阵问题

☔图像渲染

题目:

        有一幅以 m x n 的二维整数数组表示的图画 image ,其中 image[i][j] 表示该图画的像素值大小。

        你也被给予三个整数 sr ,  sc 和 newColor 。你应该从像素 image[sr][sc] 开始对图像进行 上色填充 。

        为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor 。

        最后返回 经过上色渲染后的图像 。

示例:

        输入: image = [[1,1,1],[1,1,0],[1,0,1]],sr = 1, sc = 1, newColor = 2

        输出: [[2,2,2],[2,2,0],[2,0,1]]

解析:

        在图像的正中间,(坐标(sr,sc)=(1,1)),在路径上所有符合条件的像素点的颜色都被更改成2。

注意:

        右下角的像素没有更改为2,因为它不是在上下左右四个方向上与初始点相连的像素点。

class Solution {
    int[] di = {1, 0, 0, -1};
    int[] dj = {0, 1, -1, 0};
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        boolean[][] flag = new boolean[image.length][image[0].length];
        dfs(image,flag,sr,sc,image[sr][sc],newColor);
        return image;
    }
    public void dfs(int[][] image,boolean[][] flag,int i,int j,int oldColor,int newColor){
        image[i][j] = newColor;
        flag[i][j] = true;
        //搜索上下左右
        for(int k=0;k<4;++k){
            int newI=i+di[k];
            int newJ=j+dj[k];
            //是否越界
            if(newI>=image.length||newJ>=image[0].length||newI<0||newJ<0){
                continue;
            }
            //是否需要渲染
            if(!flag[newI][newJ]&&image[newI][newJ]==oldColor){
                dfs(image,flag,newI,newJ,oldColor,newColor);
            }
        }
    }
}

☔岛屿的周长

题目:

        给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。

        网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

        岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

示例:

        输入:grid = [ [0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0] ]

·       输出:16

解释:

        它的周长是上面图片中的 16 个黄色的边

class Solution {
    //这道题的思路是,一个坐标,如果他是边界或者相邻的坐标为0,则有一个边
    public int islandPerimeter(int[][] grid) {
        int m=grid.length;
        int n=grid[0].length;
        int count=0;
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(grid[i][j]==1){
                    if(i==0||grid[i-1][j]==0){
                        count++;
                    }
                    if(i==m-1||grid[i+1][j]==0){
                        count++;
                    }
                    if(j==0||grid[i][j-1]==0){
                        count++;
                    }
                    if(j==n-1||grid[i][j+1]==0){
                        count++;
                    }
                }
            }
        }
        return count;
    }
}

☔被围绕的区域

题目:

        给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。

示例:


        输入:board = [ ["X","X","X","X"] , ["X","O","O","X"] , ["X","X","O","X"] , ["X","O","X","X"] ]

        输出:[ ["X","X","X","X"] , ["X","X","X","X"] , ["X","X","X","X"] , ["X","O","X","X"] ]

解释:

        被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。

class Solution {
    //这道题的思路是,从边界出发,把边界岛屿都更改为A,
    //再次遍历,将所有O改为X,也就是说将不是边界的岛屿全部改为X
    //再将A改回O
    public void solve(char[][] board) {
        int m = board.length;
        int n = board[0].length;
        for(int i=0;i<n;++i){
            dfs(board,0,i);
            dfs(board,m-1,i);
        }
        for(int i=1;i<m-1;++i){
            dfs(board,i,0);
            dfs(board,i,n-1);
        }
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(board[i][j]=='O'){
                    board[i][j]='X';
                }
                if(board[i][j]=='A'){
                    board[i][j]='O';
                }
            }
        }
    }
    public void dfs(char[][] board,int i,int j){
        int m = board.length;
        int n = board[0].length;
        if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != 'O') {
            return;
        }
        board[i][j] = 'A';
        dfs(board, i + 1, j);
        dfs(board, i - 1, j);
        dfs(board, i, j + 1);
        dfs(board, i, j - 1);
    }
}

☔岛屿数量

题目:

        给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

        岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

        此外,你可以假设该网格的四条边均被水包围。 

示例:

        输入:grid = [ ["1","1","0","0","0"],

                               ["1","1","0","0","0"],

                               ["0","0","1","0","0"],

                               ["0","0","0","1","1"] ]

        输出:3

class Solution {
    //思路:找到的岛屿就利用递归将整个岛屿改成2,每找到一个岛屿计数一次
    public int numIslands(char[][] grid) {
        int n = grid[0].length;
        int m = grid.length;
        int count=0;
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(grid[i][j]=='1'){
                    count++;
                    dfs(grid,i,j);
                }
            }
        }
        return count;
    }
    public void dfs(char[][] grid,int i,int j){
        int n = grid[0].length;
        int m = grid.length;
        if(i>=m||j>=n||i<0||j<0||grid[i][j]!='1'){
            return;
        }
        grid[i][j]='2';
        dfs(grid,i+1,j);
        dfs(grid,i-1,j);
        dfs(grid,i,j-1);
        dfs(grid,i,j+1);
    }
}

☔岛屿的最大面积

题目:

        给你一个大小为 m x n 的二进制矩阵 grid 。

        岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

        岛屿的面积是岛上值为 1 的单元格的数目。

        计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

示例:

 

        输入:grid = [ [0,0,1,0,0,0,0,1,0,0,0,0,0],

                               [0,0,0,0,0,0,0,1,1,1,0,0,0],

                               [0,1,1,0,1,0,0,0,0,0,0,0,0],

                               [0,1,0,0,1,1,0,0,1,0,1,0,0],

                               [0,1,0,0,1,1,0,0,1,1,1,0,0],

                               [0,0,0,0,0,0,0,0,0,0,1,0,0],

                               [0,0,0,0,0,0,0,1,1,1,0,0,0],

                              [0,0,0,0,0,0,0,1,1,0,0,0,0] ]

        输出:6

解释:

        答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。

class Solution {
    //思路:将找到的岛屿利用递归操作全部改成0,顺便记录岛屿的最大面积
    public int maxAreaOfIsland(int[][] grid) {
        int n = grid[0].length;
        int m = grid.length;
        int max=0;
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(grid[i][j]==1){
                    max=Math.max(dfs(grid,i,j),max);
                }
            }
        }
        return max;
    }
    public int dfs(int[][] grid,int i,int j){
        int n = grid[0].length;
        int m = grid.length;
        if(i>=m||j>=n||i<0||j<0||grid[i][j]!=1){
            return 0;
        }
        grid[i][j]=0;
        int count=1;
        count+=dfs(grid,i+1,j);
        count+=dfs(grid,i-1,j);
        count+=dfs(grid,i,j-1);
        count+=dfs(grid,i,j+1);
        return count;
    }
} 

☔N皇后

题目:

        n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

        给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

        每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例:

        输入:n = 4

        输出:[ [".Q..","...Q","Q...","..Q."] , ["..Q.","Q...","...Q",".Q.."] ]

解释:

        如上图所示,4 皇后问题存在两个不同的解法。

class pair{
    public int x;
    public int y;
    public pair(int x, int y){
        this.x = x;
        this.y = y;
    }
}
class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<pair>> solutions = new ArrayList<>();
        List<pair> solution = new ArrayList<>();
        nQueensBacktrack(solutions, solution, 0, n);
        return transResult(solutions, n);
    }
    void nQueensBacktrack(List<List<pair>> solutions,List<pair> solution, int curRow, int n) {
        if (curRow == n){
            List<pair> newS = new ArrayList<>();
            for(pair p : solution){
                newS.add(p);
            }
            solutions.add(newS);
        }
        for (int col = 0; col < n; ++col) {
            if (isValid(solution, curRow, col)) {
                solution.add(new pair(curRow, col));
                nQueensBacktrack(solutions, solution, curRow + 1, n);
                solution.remove(solution.size() - 1);
            }
        }
    }
    boolean isValid(List<pair> solution, int row, int col) {
        for (pair i : solution){
            if (i.y == col || i.x + i.y == row + col|| i.x - i.y == row - col){
                return false;
            }
        }
        return true;
    }
    List<List<String>> transResult(List<List<pair>> solutions, int n) {
        List<String> tmp = new ArrayList<>();
        List<List<String>> ret = new ArrayList<>();
        for (List<pair> solution : solutions) {
            List<StringBuilder> solutionString = new ArrayList<>();
            for(int i = 0; i < n; ++i){
                StringBuilder sb = new StringBuilder();
                for(int j = 0; j < n; ++j){
                    sb.append('.');
                }
                solutionString.add(sb);
            }
            for (pair i : solution) {
                solutionString.get(i.x).setCharAt(i.y, 'Q');
            }
            List<String> curRet = new ArrayList<>();
            for(StringBuilder sb : solutionString){
                curRet.add(sb.toString());
            }
            ret.add(curRet);
        }
        return ret;
    }
}

☔单词搜索

题目:

        给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

        单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

        输入:board = [ ["A","B","C","E"],["S","F","C","S"],["A","D","E","E"] ], word = "ABCCED"

        输出:true

class Solution {
    public boolean exist(char[][] board, String word) {
        char[] words = word.toCharArray();
        for(int i=0;i<board.length;++i){
            for(int j=0;j<board[0].length;++j){
                if(dfs(board,words,i,j,0)){
                    return true;
                }
            }
        }
        return false;
    }
    public boolean dfs(char[][] board,char[] words,int i,int j,int k){
        if(i<0||j<0||i>board.length-1||j>board[0].length-1||board[i][j]!=words[k]){
            return false;
        }
        if(k==words.length-1){
            return true;
        }
        board[i][j]='\0';
        boolean res = dfs(board,words,i+1,j,k+1)||dfs(board,words,i,j-1,k+1)||dfs(board,words,i,j+1,k+1)||dfs(board,words,i-1,j,k+1);
        board[i][j]=words[k];
        return res;
    }
}

🌂广度优先搜索

💧转盘锁问题

☔打开转盘锁

题目:

        你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。

        锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。

        列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

        字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1 。

示例:

        输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"

        输出:6

解释:

        可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。

class Solution {
    public int openLock(String[] deadends, String target) {
        HashSet<String> dict = new HashSet<>();
        HashSet<String> book = new HashSet<>();
        Queue<String> queue = new LinkedList<>();
        int step = 0;
        //设置哈希集合存放禁止的数字
        for(String s : deadends){
            dict.add(s);
        }
        //如果禁止数字里有起始数字,那就直接返回-1
        if(dict.contains("0000")){
            return -1;
        }
        //模板
        book.add("0000");
        queue.offer("0000");
        while(!queue.isEmpty()){
            int size = queue.size();
            while(size--!=0){
                String cur = queue.poll();
                //找到了直接返回
                if(cur.equals(target)){
                    return step;
                }
                for(int i=0;i<4;++i){
                    //分别有+操作和-操作
                    //需要注意的是回环
                    //如果当前是9,0时,向前,向后拨动需要变成最小最大
                    StringBuffer s1 = new StringBuffer(cur);
                    char ch1 = s1.charAt(i);
                    if(ch1=='9'){
                        ch1='0';
                    }else{
                        ch1++;
                    }
                    s1.setCharAt(i,ch1);
                    //没在禁止里并且没操作过就入队
                    if(!book.contains(s1.toString())&&!dict.contains(s1.toString())){
                        queue.offer(s1.toString());
                        book.add(s1.toString());
                    }
                    //另一种操作
                    StringBuffer s2 = new StringBuffer(cur);
                    char ch2 = s2.charAt(i);
                    if(ch2=='0'){
                        ch2='9';
                    }else{
                        ch2--;
                    }
                    s2.setCharAt(i,ch2);
                    if(!book.contains(s2.toString())&&!dict.contains(s2.toString())){
                        queue.offer(s2.toString());
                        book.add(s2.toString());
                    }
                }
            }
            //计数
            step++;
        }
        //完事之后都没找到,那就返回-1
        return -1;
    }
}

☔单词接龙

题目:

        字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk:

        每一对相邻的单词只差一个字母。

        对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。

        sk == endWord

        给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。

示例:

        输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]

        输出:5
解释:

        一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Queue<String> queue = new LinkedList<>();
        int step = 1;
        //找过的放在这里
        HashSet<String> book = new HashSet<>();
        //所有的放在这里
        HashSet<String> dict = new HashSet<>();

        //将给的单词全部加到dict里面去
        for(String s : wordList){
            dict.add(s);
        }

        queue.offer(beginWord);
        book.add(beginWord);

        while(!queue.isEmpty()){
            int size = queue.size();
            while(size--!=0){
                String cur = queue.poll();
                //如果当前字符等于结尾字符那就说明找到了
                if(cur.equals(endWord)){
                    return step;
                }
                //修改单词的某一个字符
                for(int i=0;i<cur.length();++i){
                    StringBuffer s = new StringBuffer(cur);
                    for(char ch = 'a';ch<='z';++ch){
                        s.setCharAt(i,ch);
                        //判断新的单词是不是在dict中,是不是不在book中
                        if(dict.contains(s.toString())&&!book.contains(s.toString())){
                            queue.offer(s.toString());
                            book.add(s.toString());
                        }
                    }
                }
            }
            step++;
        }
        return 0;
    }
}

☔最小基因变化

题目:

        基因序列可以表示为一条由 8 个字符组成的字符串,其中每个字符都是 'A'、'C'、'G' 和 'T' 之一。

        假设我们需要调查从基因序列 start 变为 end 所发生的基因变化。一次基因变化就意味着这个基因序列中的一个字符发生了变化。

        例如,"AACCGGTT" --> "AACCGGTA" 就是一次基因变化。
        另有一个基因库 bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。

        给你两个基因序列 start 和 end ,以及一个基因库 bank ,请你找出并返回能够使 start 变化为 end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 。

注意:

        起始基因序列 start 默认是有效的,但是它并不一定会出现在基因库中。

示例:

        输入:start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]

        输出:1

class Solution {
    public int minMutation(String start, String end, String[] bank) {
        Queue<String> queue = new LinkedList<>();
        int step = 0;
        //找过的放在这里
        HashSet<String> book = new HashSet<>();
        //所有的放在这里
        HashSet<String> dict = new HashSet<>();

        //将给的单词全部加到dict里面去
        for(String s : bank){
            dict.add(s);
        }

        queue.offer(start);
        book.add(start);

        while(!queue.isEmpty()){
            int size = queue.size();
            while(size--!=0){
                String cur = queue.poll();
                //如果当前字符等于结尾字符那就说明找到了
                if(cur.equals(end)){
                    return step;
                }
                //修改单词的某一个字符
                for(int i=0;i<cur.length();++i){
                    StringBuffer s = new StringBuffer(cur);
                    for(char ch = 'A';ch<='Z';++ch){
                        s.setCharAt(i,ch);
                        //判断新的单词是不是在dict中,是不是不在book中
                        if(dict.contains(s.toString())&&!book.contains(s.toString())){
                            queue.offer(s.toString());
                            book.add(s.toString());
                        }
                    }
                }
            }
            step++;
        }
        return -1;
    }
}

 💧树形结构问题

☔N叉树的层序遍历

题目:

        给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。

        树的序列化输入是用层序遍历,每组子节点都由 null 值分隔。

示例:

        输入:root = [1,null,3,2,4,null,5,6]

        输出:[[1],[3,2,4],[5,6]]

/*
// Definition for a Node.
class Node {
    public int val;
    public List<Node> children;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, List<Node> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Solution {
    //经典的广度优先了,没什么好说的,这就是典型的模板
    public List<List<Integer>> levelOrder(Node root) {
        if (root == null) {
            return new ArrayList<>();
        }
        List<List<Integer>> ans = new ArrayList<>();
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int cnt = queue.size();
            List<Integer> level = new ArrayList<Integer>();
            for (int i = 0; i < cnt; ++i) {
                Node cur = queue.poll();
                level.add(cur.val);
                for (Node child : cur.children) {
                    queue.offer(child);
                }
            }
            ans.add(level);
        }
        return ans;
    }
}

💧二维矩阵问题

☔腐烂的橘子

题目:

        在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

        值 0 代表空单元格;值 1 代表新鲜橘子;值 2 代表腐烂的橘子。

        每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

        返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。

示例:

        输入:grid = [[2,1,1],[1,1,0],[0,1,1]]

        输出:4

//橘子的坐标
class Pair{
    int x;
    int y;
    public Pair(int x,int y){
        this.x=x;
        this.y=y;
    }
}
class Solution {
    public int orangesRotting(int[][] grid) {
        int n = grid.length;
        int m = grid[0].length;
        Queue<Pair> queue = new LinkedList<>();
        //刚开始就腐烂的橘子,全部入队
        for(int i=0;i<n;++i){
            for(int j=0;j<m;++j){
                if(grid[i][j]==2){
                    queue.offer(new Pair(i,j));
                }
            }
        }
        int[][] nextP = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
        int count=0;
        //模板
        while(!queue.isEmpty()){
            int size = queue.size();
            boolean flag=false;
            while(size--!=0){
                Pair cur = queue.poll();
                for(int i =0;i<4;++i){
                    int nx = cur.x+nextP[i][0];
                    int ny = cur.y+nextP[i][1];
                    if(nx>n-1||ny>m-1||nx<0||ny<0){
                        continue;
                    }
                    if(grid[nx][ny]==1){
                        grid[nx][ny]=2;
                        flag=true;
                        queue.offer(new Pair(nx,ny));
                    }
                }
            }
            //有新橘子被腐烂,就计数
            if(flag){
                count++;
            }
        }
        //还有没腐烂的,就返回-1
        for(int i=0;i<n;++i){
            for(int j=0;j<m;++j){
                if(grid[i][j]==1){
                    return -1;
                }
            }
        }
        return count;
    }
}

😀总结

许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。

而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

🌈深度优先算法模板

dfs(){

        判断边界

        for(){

                尝试当下的每一种可能

                确定一种可能之后,继续下一步dfs

                撤销操作

        }

}

🌈广度优先算法模板

bfs(){

        创建一个队列,入队根节点

        while(队列不为空){

                int size = 这一层长度

                while(这一层){

                        找相关节点

                        符合条件,相关节点入队

                }

        }

}

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值