硅基计划4.0 算法 递归&回溯


文章目录


一、电话号码数字组合

题目链接

这一题我们说白了就是对题目中给的数字进行解析,我们画一个决策树就好

有了这个决策树,我们就知道函数怎么设计了

  • 既然要映射关系,那我们就要搞一个字符串数组,下标对应不同数字映射的字母
  • 肯定也要有一个path变量去叶子节点记录结果
  • 参数就是题目给的数字字符串,以及我们提取这个数字字符串的位置
java 复制代码
class Solution {
    String [] letter = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    List<String> list;
    StringBuilder path;
    public List<String> letterCombinations(String digits) {
        list = new ArrayList<>();
        path = new StringBuilder();
        if(digits.equals("")){
            return list;
        }
        dfs(digits,0);
        return list;
    }

    private void dfs(String digits,int pos){
        if(pos == digits.length()){
            //说明到头了,进行添加结果
            list.add(path.toString());
            return;
        }
        //获取当前数字映射的字母
        String current = letter[digits.charAt(pos)-'0'];
        //开始递归
        for(int i = 0;i < current.length();i++){
            path.append(current.charAt(i));
            dfs(digits,pos+1);
            //回溯现场
            path.deleteCharAt(path.length()-1);
        }
    }
}

二、括号生成

题目链接

首先我们来明确一下什么是有效的括号,那么整体上左括号数量=右括号数量,这不是废话吗!!

还有,我们要保证从头开始的子串中左括号数量必须 >= 右括号数量

比如(()))这个就是一个非法括号,而((())这个是合法的括号(前提是没有到最后位置)

因此,我们还是通过画决策树明确剪枝关系

因此,我们也需要一个path变量记录路径,并且在回溯的时候要恢复现场,还原,并且根据决策树我们还涉及到一些剪枝操作

java 复制代码
class Solution {
    StringBuilder path = new StringBuilder();
    int left;
    int right;
    int count;
    List<String> list = new ArrayList<>();
    public List<String> generateParenthesis(int n) {
        count = n;
        dfs();
        return list;
    }

    private void dfs(){
        if(right == count){
            list.add(path.toString());
            return;
        }
        if(left < count){
            path.append("(");
            left++;
            dfs();
            path.deleteCharAt(path.length()-1);
            left--;
        }
        if(right < left){
            path.append(")");
            right++;
            dfs();
            path.deleteCharAt(path.length()-1);
            right--;
        }
    }
}

三、组合

题目链接

这一题不能枚举重复结果,即2,44,2是同一种情况

我们同样画一个决策树

那要怎么保证不能重复呢,诶,我们可以每次枚举的时候,从上一次递归的下一个位置开始枚举

java 复制代码
class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> list = new ArrayList<>();
    int num;
    int knum;
    public List<List<Integer>> combine(int n, int k) {
        if(n <= 0){
            return list;
        }
        num = n;
        knum = k;
        dfs(1);
        return list;
    }

    private void dfs(int pos){
        if(path.size() == knum){
            list.add(new ArrayList<>(path));
            return;
        }
        for(int i = pos;i <= num;i++){
            path.add(i);
            dfs(i+1);
            path.remove(path.size()-1);
        }
    }
}

四、二叉树的最大宽度

题目链接

这道题如果你直接硬来的话,它可能会给你1500个单分支树的节点,因此我们可以利用的思想,给每个节点编号,这样我们求宽度的时候,就可以拿当前层最右侧的节点编号 - 当前层最左侧的节点编号

如果我们根节点编号从0开始,左孩子编号就是2x+1,右孩子编号就是2x+2

如果我们根节点编号从1开始,左孩子编号就是2x,右孩子就是2x+1

虽然我们编号下标可能会溢出,但是我们做的差是不可能溢出的

同时,我们使用数组去模拟队列,避免头部删除的时候增加时间复杂度

1. 广度优先搜索(BFS)解法

java 复制代码
/**
 * 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 {
    public int widthOfBinaryTree(TreeNode root) {
        //数组模拟队列
        List<Pair<TreeNode,Integer>> queue = new ArrayList<>();
        //根节点从1开始
        queue.add(new Pair<TreeNode,Integer>(root,1));
        //记录结果
        int ret = 0;
        while(!queue.isEmpty()){
            Pair<TreeNode,Integer> cur1 = queue.get(0);
            Pair<TreeNode,Integer> cur2 = queue.get(queue.size()-1);
            ret = Math.max(ret,cur2.getValue()-cur1.getValue()+1);
            //开始为下一层进队
            //使用临时队列存储,后续直接赋值,避免头部删除增加时间复杂度
            List<Pair<TreeNode,Integer>> tmps = new ArrayList<>();
            for(Pair<TreeNode,Integer> tmp : queue){
                TreeNode node = tmp.getKey();
                int index = tmp.getValue();
                if(node.left != null){
                    tmps.add(new Pair<TreeNode,Integer>(node.left,index*2));
                }
                if(node.right != null){
                    tmps.add(new Pair<TreeNode,Integer>(node.right,index*2+1));
                }
            }
            //更新到下一层
            queue = tmps;
        }
        return ret;
    }
}

2. 深度优先搜索(DFS)解法

java 复制代码
/**
 * 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 {
    //哈希表记录每一层最左侧节点的下标
    HashMap<Integer,Integer> hash = new HashMap<>();
    int maxWide;
    public int widthOfBinaryTree(TreeNode root) {
        dfs(root,0,1);
        return maxWide;
    }

    private void dfs(TreeNode root,int depth,int index){
        if(root == null){
            return;
        }
        hash.putIfAbsent(depth,(int)index);
        //计算宽度
        maxWide = Math.max(maxWide,(int)(index-hash.get(depth)+1));
        //递归左右子树
        dfs(root.left,depth+1,index*2);
        dfs(root.right,depth+1,index*2+1);
    }
}

五、目标和

题目链接

这一题我们采用暴力搜索策略


这里顺便提一嘴,如果我们的path变量是诸如int类型,就使用参数传递

如果我们的path变量是诸如List类型,就使用全局变量传递

当然具体哪种还是看个人习惯


我们还是一样画一个决策树

java 复制代码
class Solution {
    int path;
    int targets;
    int count;
    public int findTargetSumWays(int[] nums, int target) {
        //path作为全局变量传递
        targets = target;
        dfs(nums,0);
        return count;
    }

    private void dfs(int [] array,int pos){
        if(pos == array.length){
            if(path == targets){
                count++;
            }
            return;
        }
        //加法
        path += array[pos];
        dfs(array,pos+1);
        path -= array[pos];//恢复现场

        //减法
        path -= array[pos];
        dfs(array,pos+1);
        path += array[pos];//恢复现场
    }
}

六、组合总和

题目链接

1. 数字选择法

这道题关键就是在于可以选择重复元素,并且2 2 33 2 2视为同一种情况

我们还是来画决策树

java 复制代码
class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> list = new ArrayList<>();
    int targets;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        targets = target;
        dfs(candidates,0,0);
        return list;
    }

    private void dfs(int [] array,int pathSum,int pos){
        //递归出口一:到达数组最后一个元素
        if(pathSum == targets){
            list.add(new ArrayList<>(path));
            return;
        }
        //递归出口二:路径和提前超过了目标值
        if(pathSum > targets || pos == array.length){
            return;
        }
        for(int i = pos;i < array.length;i++){
            path.add(array[i]);
            dfs(array,pathSum+array[i],i);
            path.remove(path.size()-1);
        }
    }
}

2. 数字选择次数法

其实我们还有另外一种解法,就是考虑每个数字选择几次

我们再来画一个决策树

请注意,每次我们回溯现场的时候,一定要等当前数字使用次数全部枚举完毕后,再进行恢复现场

具体怎么恢复,我们递归的时候是怎么加的数字,我们回溯的时候就相反地怎么减去数字

java 复制代码
class Solution {
    List<Integer> path = new ArrayList<>();
    List<List<Integer>> list = new ArrayList<>();
    int targets;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        targets = target;
        dfs(candidates,0,0);
        return list;
    }

    private void dfs(int [] array,int pathSum,int pos){
        if(pathSum == targets){
            list.add(new ArrayList<>(path));
            return;
        }
        if(pathSum > targets || pos == array.length){
            return;
        }
        //枚举几个当前pos位置的数字
        for(int i = 0;i*array[pos] <= targets;i++){
            if(i != 0){
                //只有i不为0才去添加值
                //因为i为0添加值也没用
                path.add(array[pos]);
            }
            dfs(array,pathSum+i*array[pos],pos+1);
        }
        //恢复现场,要把我们当前这一层添加的所有元素都删除,好让其他层保持上一层递归状态
        for(int i = 1;i*array[pos] <= targets;i++){
            path.remove(path.size()-1);
        }
    }
}

七、字母大小写全排列

题目链接

这题我们直接画一个决策树

java 复制代码
class Solution {
    List<String> list = new ArrayList<>();
    StringBuilder path = new StringBuilder();
    public List<String> letterCasePermutation(String s) {
        char [] ss = s.toCharArray();
        dfs(ss,0);
        return list;
    }

    private void dfs(char [] ss,int pos){
        if(pos == ss.length){
            list.add(path.toString());
            return;
        }
        //不改变大小写
        path.append(ss[pos]);
        dfs(ss,pos+1);
        //恢复现场
        path.deleteCharAt(path.length()-1);

        //改变大小写,并且是字母才改变
        if(Character.isLetter(ss[pos])){
            //判断此时字母是大写还是小写
            if(Character.isLowerCase(ss[pos])){
                path.append(Character.toUpperCase(ss[pos]));
            }else{
                path.append(Character.toLowerCase(ss[pos]));
            }
            dfs(ss,pos+1);
            //恢复现场
            path.deleteCharAt(path.length()-1);
        }
    }
}

八、优美的排列

题目链接

这题就在于不能选择重复元素,因此我们可以使用一个boolean类型数组,标记当前数字有没有被使用过

直接看代码吧

java 复制代码
class Solution {
    boolean[] isUse;
    int count;
    public int countArrangement(int n) {
        isUse = new boolean[n+1];
        dfs(1,n);
        return count;
    }

    private void dfs(int pos,int num){
        if(pos > num){
            //说明之前都是优美排列
            count++;
            return;
        }
        for(int i = 1;i <= num;i++){
            if(!isUse[i] && (i % pos == 0 || pos % i == 0)){
                isUse[i] = true;
                dfs(pos+1,num);
                //恢复现场
                isUse[i] = false;
            }
        }
    }
}

九、N皇后------Hard

题目链接

我们首先来明确皇后攻击其他皇后的规则

首先,当前行和列不能有其他皇后,并且主对角线和副对角线也不能有皇后

我们先来画决策树

因此棋盘大小是3是不可以的,其实我们决策树里边就可以看出来我们的具体函数设计

即我们每行每行的去考虑皇后放在哪个位置,检查行列对角线,一直枚举到超出棋盘范围终止

但是如果我们直接循环四次查看行列对角线符合要求,这世界复杂度也太高了,因此我们可以类似哈希表,搞三个boolean数组

一个数组表示当前列是否存在皇后(我们是逐行递归的,行不用看)

另一个数组是主对角线数组,这个利用到我们数学原理

因此只要我们y-x=b,这个b值相同,就说明我们在同一条线上

但是,如果y-x < 0怎么办呢,我们可以加上偏移量,让其结果永远是正数

y - x + 棋盘大小 = b


同理副对角线也是这样搞法,只不过不用担心负值

y + x = b


同时,我们对角线数组大小必须是2倍棋盘大小,不然的话可能会越界

java 复制代码
class Solution {
    //列数组,标识每一列的皇后出现情况
    boolean [] cols;
    //主对角线数组,表示每一个主对角线皇后出现情况
    boolean [] dig1;
    //副对角线,表示每一个副对角线皇后出现情况
    boolean [] dig2;
    //统计结果
    List<List<String>> list = new ArrayList<>();
    //表示当前棋盘大小
    int size;
    //需要一个全局变量路径去记录结果
    char [][] path;
    public List<List<String>> solveNQueens(int n) {
        cols = new boolean[n];
        //使用y-x=b这一特性判断,因此需要两倍n的大小
        dig1 = new boolean[2*n];
        //使用y+x=b这一特性判断,因此需要两倍n的大小
        dig2 = new boolean[2*n];
        //初始化路径
        path = new char[n][n];
        //初始化棋盘大小
        size = n;
        //初始棋盘
        for(int i = 0;i < n;i++){
            for(int j = 0;j < n;j++){
                path[i][j] = '.';
            }
        }
        //因此需要一个坐标
        dfs(0);
        return list;
    }

    private void dfs(int row){
        //当我们枚举到最后一行的下一行(即越界时候)停止枚举
        if(row == size){
            //能到达这里,说明是一种合法情况
            //遍历path结果集
            List<String> tmp = new ArrayList<>();
            for(int i = 0;i < size;i++){
                tmp.add(new String(path[i]));
            }
            list.add(new ArrayList<>(tmp));
            return;
        }
        //开始逐行枚举,现在针对当前行的每一列进行枚举
        for(int i = 0;i < size;i++){
            if(!cols[i] && !dig1[row-i+size] && !dig2[row+i]){
                //说明是一个合法位置
                path[row][i] = 'Q';
                //设置数组
                cols[i] = dig1[row-i+size] = dig2[row+i] = true;
                dfs(row+1);
                //恢复现场
                path[row][i] = '.';
                cols[i] = dig1[row-i+size] = dig2[row+i] = false;
            }
        }
    }
}

十、有效的数独

题目链接

我们可以定义一个row的二维数组,row[i][j]表示第i行是否出现j这个数字

同时我们可以定义一个col的二维数组,col[i][j]表示第i列是否出现j这个数字

再者我们可以定义一个grid的三维数组,grid[i][j][num]表示第i行且第j列个九宫格内是否出现num这个数字

那我们要如何确定自己在哪个九宫格之中呢,我们仅需把当前坐标比如是(4,2)除以3,即第1行第0列的九宫格,即可找到位置

java 复制代码
class Solution {
    public boolean isValidSudoku(char[][] board) {
        int size = 9;
        // 记录每行数字出现情况
        boolean[][] row = new boolean[size][size + 1];
        // 记录每列数字出现情况  
        boolean[][] col = new boolean[size][size + 1];
        // 记录每个3x3宫格数字出现情况
        boolean[][][] grid = new boolean[3][3][size + 1]; 
        // 遍历整个数独板
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                char c = board[i][j];
                // 跳过空白格
                if (c == '.') {
                    continue;
                }
                // 字符数字转整型数字
                int num = c - '0'; // 或者使用 Character.getNumericValue(c)
                // 检查当前数字是否在行、列、宫格中重复出现
                if (row[i][num] || col[j][num] || grid[i / 3][j / 3][num]) {
                    return false; // 发现重复,立即返回false
                }
                // 标记数字已出现
                row[i][num] = true;
                col[j][num] = true;
                grid[i / 3][j / 3][num] = true;
            }
        }
        return true; // 所有检查通过
    }
}

十一、解数独

题目链接

这题难点就在于我们要对每行每列(即每个单元格都要判断),并且跟上一题一样,也是使用三个数组标记

但是如果我们填入某个合法数字,后续也可能会导致数字无法填入,就要进行回溯操作

那我们要怎么知道我们填的是否正确呢,我们可以使用一个返回值,如果到了最后整个棋盘都填成功了,我们就一直向上返回true,反之就返回false,以便进行恢复现场操作

1. 函数有返回值的写法

java 复制代码
class Solution {
    boolean [][] row;
    boolean [][] col;
    boolean [][][] grid;
    public void solveSudoku(char[][] board) {
        row = new boolean[9][10];
        col = new boolean[9][10];
        grid = new boolean[3][3][10];
        //初始化原本就存在数独表中的数字
        for(int i = 0;i < 9;i++){
            for(int j = 0;j < 9;j++){
                if(board[i][j] != '.'){
                    int num = board[i][j] - '0';
                    row[i][num] = true;
                    col[j][num] = true;
                    grid[i/3][j/3][num] = true;
                }
            }
        }
        dfs(board);
    }

    private boolean dfs(char[][] board){
        //遍历每一个单元格
        for(int rows = 0;rows < 9;rows++){
            for(int cols = 0;cols < 9;cols++){
                if(board[rows][cols] == '.'){
                    //不是数字才开始填入
                    for(int num = 1;num <= 9;num++){
                        //开始尝试1-9数字
                        if(!row[rows][num] && !col[cols][num] && !grid[rows/3][cols/3][num]){
                            //先尝试放入数字
                            board[rows][cols] = (char)('0' + num);
                            //设置状态
                            row[rows][num] = true;
                            col[cols][num] = true;
                            grid[rows/3][cols/3][num] = true;
                            //递归,自动会找到后续的单元格,尝试填入数字
                            //如果我们最后发现整个数独表填满了,直接向上返回
                            if(dfs(board)){
                                return true;
                            }
                            //此时如果我们填入的数字不合法,我们就进行恢复现场操作
                            board[rows][cols] = '.';
                            //同时不要忘了去设置状态
                            row[rows][num] = false;
                            col[cols][num] = false;
                            grid[rows/3][cols/3][num] = false;
                        }
                    }
                    //说明我们填入的1-9数字不合法,就要给上一层返回false
                    return false;
                }
            }
        }
        //说明整个棋盘每个空格我们都填完了,因此直接返回true
        return true;
    }
}

2. 函数无返回值写法

java 复制代码
class Solution {
    //表示棋盘大小
    int size;
    //全局变量棋盘
    char[][] boards;
    //检查行是否存在相同的数字
    boolean[][] row;
    //检查列是否有存在相同的数字
    boolean[][] col;
    //检查九宫格内是否存在相同的数字
    boolean[][][] grid;
    //标记是否找到解
    boolean isFound = false;
    
    public void solveSudoku(char[][] board) {
        boards = board;
        size = 9; // 数独固定为9×9
        row = new boolean[size][10]; //数字1-9,所以需要10个位置
        col = new boolean[size][10];
        grid = new boolean[3][3][10]; //九宫格是3×3的
        
        //初始化已有数字
        for(int i = 0; i < size; i++){
            for(int j = 0; j < size; j++){
                char ch = board[i][j];
                if(ch != '.'){
                    int num = ch - '0';
                    row[i][num] = true;
                    col[j][num] = true;
                    grid[i/3][j/3][num] = true;
                }
            }
        }
        dfs(0,0);
    }
    
    private void dfs(int posx, int posy){
        if(isFound){
            //整个表求解完毕
            return;
        }
        if(posx == size){
            //找到了解
            isFound = true;
            return;
        }
        if(posy == size){
            //说明当前行找完了
            dfs(posx+1,0);
            return;
        }
        if(boards[posx][posy] != '.'){
            //当前位置已经有数字了
            dfs(posx,posy+1);
            return;
        }
        
        //每个单元格看,前提要是非数字才去判断
        for(int i = 1; i <= 9; i++){
            //遍历1-9数字
            if(!row[posx][i] && !col[posy][i] && !grid[posx/3][posy/3][i]){
                boards[posx][posy] = (char)('0' + i);
                row[posx][i] = true;
                col[posy][i] = true;
                grid[posx/3][posy/3][i] = true;
                
                //去当前行的下一列看看
                dfs(posx,posy+1);
                
                if(isFound){
                    //找到了解直接向上返回
                    return;
                }
                
                /*
                1.递归到达某个单元格,所有1-9的数字都违反规则
                2.循环结束,该层递归返回false
                3.返回到上一层递归,执行回溯代码
                4.我们有理由认为这个数字的填入导致后续单元格数字无法填入
                */
                //回溯现场
                boards[posx][posy] = '.';
                row[posx][i] = false;
                col[posy][i] = false;
                grid[posx/3][posy/3][i] = false;
            }
        }
    }
}

十二、单词搜索

题目

这题我们可以针对性的一行行扫描,具体看图

因此我们函数设计就可以变成每一次递归的时候,尝试去上下左右四个方位寻找目标字符,找到了就递归,找不到就回溯

并且我们搜索也是不能走重复路径的,因此同样使用一个boolean标记当前位置是否走过


但是我们要进行四个位置递归,开销太大,我们可以搞两个向量数组,表示四个方位,循环就循环四次就好

向量数组x方向:{0,0,1,-1} y方向:{1,-1,0,0},注意x和y方向数组要一一对应,能够表示当前位置的上下左右四个位置


你肯定想说,我不一定要用到向量数组啊,但是如果题目要求不能修改原数组呢


java 复制代码
class Solution {
    boolean [][] isUse;
    char [][] boards;
    int wordLength;
    char[] words;
    int wide;
    int height;
    public boolean exist(char[][] board, String word){
        wide = board[0].length;
        height = board.length;
        if(height == 0){
            return false;
        }
        wordLength = word.length();
        words = word.toCharArray();
        isUse = new boolean[height][wide];
        //使用全局棋盘,为了在参数传递的时候节省递归开销
        boards = board;
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                //寻找字符串中第一个字符
                if(board[i][j] == word.charAt(0)){
                    //就从此位置开始找,从字符串的第二个字符开始找
                    //先标记这个位置使用过
                    isUse[i][j] = true;
                    if(dfs(i,j,1)){
                        //如果从此位置开始后续的字符都可以被找到
                        //我们就直接返回true
                        return true;
                    }
                    //说明上面判断不合法,继续往后寻找
                    isUse[i][j] = false;
                }
            }
        }    
        //到了这里说你整个表中并不存在和字符串中第一个字符匹配的字符
        //直接返回false表示结果
        return false;
    }

    //为避免多次递归,对于矩阵,我们使用一个向量数组表示各个位置
    //上下两组一一对应
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};

    //posX表示第几行,poxY表示第几列
    private boolean dfs(int posx,int posy,int strPos){
        if(strPos == wordLength){
            //说明此时到了字符串的最后一个字符,返回true
            return true;
        }
        //使用循环直接去枚举四个方向
        for(int i = 0;i < 4;i++){
            //此时的位置,我们使用向量作为偏移量
            int curX = posx+x[i];
            int curY = posy+y[i];
            //判断当前位置的合法性
            if(curX >= 0 && curX < height && curY >= 0 && curY < wide && !isUse[curX][curY] && boards[curX][curY] == words[strPos]){
                //标记当前位置被使用过了
                isUse[curX][curY] = true;
                //进行递归
                if(dfs(curX,curY,strPos+1)){
                    //说明后续的都是正确的,我们直接向上返回
                    return true;
                }
                //说明此时是一种错误的走法,恢复现场
                isUse[curX][curY] = false;
            }
        }
        //到了这里说明四个方向都不符合要求,再一次回退
        return false;
    }
}

十三、黄金矿工

题目链接

这题和单词搜索简直是一模一样,因此就不啰嗦了

java 复制代码
class Solution {
    //同理使用一个bool数组表示当前位置是否被使用过
    boolean[][] isUse;
    int wide;
    int height;
    int ret;

    public int getMaximumGold(int[][] grid) {
        wide = grid[0].length;
        height = grid.length;
        isUse = new boolean[height][wide];
        //选取一个不为0的地点开始挖矿
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < wide; j++) {
                if (grid[i][j] != 0) {
                    //从这个地方开始挖,先标记当前位置状态
                    isUse[i][j] = true;
                    //先把当前位置黄金加上
                    dfs(grid, i, j, grid[i][j]);
                    //恢复现场,从其他位置开始挖掘
                    isUse[i][j] = false;
                }
            }
        }
        return ret;
    }

    //同理为避免多次递归,我们使用向量数组
    int[] x = { 0, 0, -1, 1 };
    int[] y = { 1, -1, 0, 0 };

    //posX表示第几行,posYB表示第几列
    private void dfs(int[][] grid, int posx, int posy, int path) {
        ret = Math.max(ret, path);
        //同理使用循环枚举四个方向
        for (int i = 0; i < 4; i++) {
            int curX = posx + x[i];
            int curY = posy + y[i];
            if (curX >= 0 && curX < height && curY >= 0 && curY < wide && !isUse[curX][curY] && grid[curX][curY] != 0) {
                //先统计结果
                path += grid[curX][curY];
                //再设置状态
                isUse[curX][curY] = true;
                //递归下一次
                dfs(grid, curX, curY, path);
                //恢复现场
                isUse[curX][curY] = false;
                //不要忘记path也要恢复
                path -= grid[curX][curY];
            }
        }
    }
}

十四、不同路径III

题目链接

这题难就难在要把所有0的位置都走一遍,并且还不能重复,并且-1的位置还不能走

这题我们采用暴力搜索方式,先扫描整个表,寻找开始位置和结束位置

再去统计0的个数,这决定了我们走到终点时,我们实际走到步数和理想步数能不能对的上

java 复制代码
class Solution {
    //标记某个位置是否已经走过
    boolean [][] isUse;
    //起始位置
    int startX,startY;
    int endX,endY;
    //统计0的个数,也就是预期走多少步
    int step;
    //定义计数器
    int count;
    int wide;
    int height;
    
    public int uniquePathsIII(int[][] grid) {
        //废话不多说,直接开始暴力搜索枚举
        height = grid.length;
        wide = grid[0].length;
        isUse = new boolean[height][wide];
        step = 0; // 重置step
        count = 0; // 重置count
        
        //先扫描一遍整个表,统计0的个数,并且明确起始位置
        for(int i = 0;i < height;i++){
            for(int j = 0;j < wide;j++){
                if(grid[i][j] == 0){
                    step++;
                }else if(grid[i][j] == 1){
                    startX = i;
                    startY = j;
                }else if(grid[i][j] == 2){
                    endX = i;
                    endY = j;
                }
            }
        }
        isUse[startX][startY] = true;
        //开始递归,从开始位置开始
        dfs(grid,startX,startY,0);
        return count;
    }

    //同样地使用向量数组
    int [] x = {0,0,1,-1};
    int [] y = {1,-1,0,0};

    //同理posx表示行,posy表示列,path表示路径上0的个数
    private void dfs(int [][] grid,int posx,int posy,int path){
        // 先检查是否到达终点
        if(posx == endX && posy == endY){
            //如果遇到了数字2,就到了终点,检查结果
            if(path == step){
                count++;
            }
            return;
        }
        
        //枚举四个方向
        for(int i = 0;i < 4;i++){
            int curX = posx+x[i];
            int curY = posy+y[i];
            
            // 检查边界、是否访问过、是否是障碍物
            if(curX >= 0 && curX < height && curY >= 0 && curY < wide && 
               !isUse[curX][curY] && grid[curX][curY] != -1){
                
                // 标记为已访问
                isUse[curX][curY] = true;
                
                // 计算新的路径长度:如果是0则加1,否则保持不变
                int newPath = path;
                if(grid[curX][curY] == 0) {
                    newPath = path + 1;
                }
                
                // 递归到下一个位置
                dfs(grid, curX, curY, newPath);
                
                //恢复现场
                isUse[curX][curY] = false;
            }
        }
    }
}

十五、找出所有子集异或总和再求和

题目链接

决策树就不用画了,这一题就是子集那道题的综合版本

找子集就是我们刚开始传入0下标,我们每一次递归这个下标++就好

然后我们在回溯的时候,要记得恢复现场,可以再异或当前位置的数字,达到消除的效果

java 复制代码
class Solution {
    int sum;
    int path;
    public int subsetXORSum(int[] nums) {
        subsetXORSumChild(nums,0);
        return sum;
    }

    private void subsetXORSumChild(int [] nums,int pos){
        //统计每一层的异或和
        sum += path;
        for(int i = pos;i < nums.length;i++){
            path ^= nums[i];
            subsetXORSumChild(nums,i+1);
            //向上回溯的时候,要把当前层多余的元素消除
            //因为刚刚在sum中就已经把每一层的结果都统计到了
            path ^= nums[i];
        }
    }
}

十六、全排列II

题目链接

这题就是全排列I的升级版本,要求同一个位置不能选择重复的数字,我们还是来换一个决策树

代码实在看不明白可以看我画的决策树,自己找几种情况带入就好了

java 复制代码
class Solution {
    List<List<Integer>> ret;
    List<Integer> path;
    boolean [] isUse;
    public List<List<Integer>> permuteUnique(int[] nums) {
        isUse = new boolean[nums.length];
        ret = new ArrayList<>();
        path = new ArrayList<>();
        Arrays.sort(nums);
        permuteUniqueChild(nums,0);
        return ret;
    }

    private void permuteUniqueChild(int [] nums,int pos){
        if(pos == nums.length){
            ret.add(new ArrayList<>(path));
            return;
        }
        //正常遍历数组
        for(int i = 0;i < nums.length;i++){
            if((isUse[i] == true || (i != 0 && nums[i] == nums[i-1] && isUse[i-1] == false))){
            //剪去不合法分支,其中i != 0 是为了保证i-1不越界
                continue;
            }
            path.add(nums[i]);
            isUse[i] = true; 
            permuteUniqueChild(nums,pos+1);
            //恢复现场
            isUse[i] = false;
            //剪枝
            path.remove(path.size()-1);
        }
        /*
        for循环也可以这样写,即只看合法分支
        for(int i = 0;i < nums.length;i++){
            if(!isUse[i] && *i == 0 || nums[i] != nums[i-1] && isUse[i-1]){
                path.add(nums[i]);
                isUse[i] = true; 
                permuteUniqueChild(nums,pos+1);
                //恢复现场
                isUse[i] = false;
                //剪枝
                path.remove(path.size()-1);
            }
        */
    }
}

希望本篇文章对您有帮助,有错误您可以指出,我们友好交流


END

相关推荐
念安jy1 小时前
SDUT数据结构与算法pta--排序
算法
大江东去浪淘尽千古风流人物1 小时前
【MSCKF】零空间 UpdaterHelper::nullspace_project_inplace 的实现细节,MSCKF边缘化含义
算法·性能优化·vr·dsp开发·mr
AndrewHZ1 小时前
【图像处理基石】图像处理中的色彩经典算法原理与实战解析
图像处理·算法·计算机视觉·白平衡·色彩科学·经典算法·k means
Dev7z1 小时前
基于Matlab多算法的图像增强与客观质量评价系统
人工智能·算法·matlab
Blossom.1181 小时前
基于扩散模型的视频生成优化:从Stable Diffusion到AnimateDiff的显存革命
人工智能·深度学习·学习·决策树·搜索引擎·stable diffusion·音视频
xcLeigh1 小时前
【新】Rust入门:基础语法应用
开发语言·算法·rust
小年糕是糕手1 小时前
【C++同步练习】类和对象(一)
java·开发语言·javascript·数据结构·c++·算法·排序算法
小O的算法实验室1 小时前
2025年IJPR SCI2区,基于混合邻域结构的高效稳定智能调度算法用于柔性作业车间调度,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
小年糕是糕手1 小时前
【C++同步练习】类和对象(二)
java·开发语言·javascript·数据结构·c++·算法·ecmascript