2023-04-19 算法面试中常见的递归和回溯问题

news2024/12/22 20:34:30

递归和回溯

0 递归与回溯的异同

参考文章

  • 递归与回溯
  • 递归与回溯的理解
  • 回溯和递归区别

比较

递归回溯
定义为了描述问题的某一状态,必须用到该状态的上一状态,而描述上一状态,又必须用到上一状态的上一状态……这种用自已来定义自己的方法,称为递归定义。形式如 f(n) = n*f(n-1), if n=0,f(n)=1.从问题的某一种可能出发, 搜索从这种情况出发所能达到的所有可能, 当这一条路走到” 尽头 “的时候, 再倒回出发点, 从另一个可能出发, 继续搜索. 这种不断” 回溯 “寻找解的方法, 称作” 回溯法 “。
不同递归是一种算法结构,递归会出现在子程序中自己调用自己或间接地自己调用自己。最直接的递归应用就是计算连续数的阶乘,计算规律:n!=(n-1)!*n。回溯是一种算法思想,可以用递归实现。通俗点讲回溯就是一种试探,类似于穷举,但回溯有“剪枝”功能,比如求和问题。给定7个数字,1 2 3 4 5 6 7,求和等于7的组合,从小到大搜索,选择1+2+3+4 =10>7,已经超过了7,之后的5 6 7就没必要在继续了,这就是一种搜索过程的优化。如果还有不清楚的可以看一下8皇后问题。
问题举例玩转算法面试第7章_二叉树与递归玩转算法面试第8章_递归与回溯

举例

用一个比较通俗的说法来解释递归和回溯:
我们在路上走着,前面是一个多岔路口,因为我们并不知道应该走哪条路,所以我们需要尝试。尝试的过程就是一个函数。

  • 我们选择了一个方向,后来发现又有一个多岔路口,这时候又需要进行一次选择。所以我们需要在上一次尝试结果的基础上,再做一次尝试,即在函数内部再调用一次函数,这就是递归的过程。
  • 这样重复了若干次之后,发现这次选择的这条路走不通,这时候我们知道我们上一个路口选错了,所以我们要回到上一个路口重新选择其他路,这就是回溯的思想。

1~2 树形问题与回溯

17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

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

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

第17号问题

class Solution {
    // 每个按键(数组的下标)对应的可能的字符串,0和1不对应任何字符,所以这里为空
    String[] letterMap = {
            "", // 0
            "", // 1
            "abc", // 2
            "def", // 3
            "ghi", // 4
            "jkl", // 5
            "mno", // 6
            "pqrs", // 7
            "tuv",  // 8
            "wxyz"  // 9
    };

    public List<String> letterCombinations(String digits) {
        // 存储最终结果的列表
        List<String> result = new ArrayList<>();
        if ("".equals(digits)){
            return result;
        }
        findCombinations(digits, 0, "", result);
        return result;
    }

    /**
     * 寻找digits[index]匹配的字母,获得digits[0...index]对应的解
     *
     * @param digits 原始数字字符串
     * @param index 要看digits的哪一个数字
     * @param s      s保存了此时从digits[0...index-1]翻译得到的一个字母字符串
     * @param result 保存最终可能的字符串
     */
    private void findCombinations(String digits, int index, String s, List<String> result) {
        // 所有数字都遍历完了,递归退出
        if (index == digits.length()) {
            result.add(s);
            return;
        }
        // 拿到index对应的数字字符
        char c = digits.charAt(index);
        // 获取当前数字字符可能对应的键盘上的字符串
        String lettersStr = letterMap[c - '0'];
        // 第当前数字对应的字符串进行遍历拼接
        for (int i = 0; i < lettersStr.length(); i++) {
            findCombinations(digits, index + 1, s + lettersStr.charAt(i), result);
        }
    }
}

93.复原IP地址

class Solution {
    private boolean validId(String ip) {
        if (ip.length() > 1 && ip.charAt(0) == '0') {
            return false;
        }
        if (ip.length() > 3 || Integer.parseInt(ip) > 255) {
            return false;
        }
        return true;
    }

    private String concatList(List<String> numList) {
        StringBuilder sb = new StringBuilder();
        for (String numStr : numList) {
            sb.append(numStr).append(".");
        }
        // 最后的一个.要去掉
        return sb.substring(0, sb.length() - 1);
    }

    // 把s分成四分,判断这四份组成的ip的合理性
    // 递归过程:不断取字符串的前几个字符,每出现一个合法的字符串就接着往下取
    private void findIp(String s, List<String> numList, List<String> result) {
        if (numList.size() == 4) {
            result.add(concatList(numList));
            // 当s字符串已经被分割成空时,分割完毕,退出本层递归即可
            return;
        }
        for (int i = 1; i <= s.length(); i++) {
            if (numList.size() == 3) {
                // 前面已经分成3份了,这里直接把剩下的作为IP即可
                i = s.length();
            }
            String tmp = s.substring(0, i);
            if (!validId(tmp)) {
                continue;
            }
            numList.add(tmp);
            // i往后的字符串
            findIp(s.substring(i), numList, result);
            // 退出上一层递归,就要从numList中删除最后一个num
            numList.remove(numList.size() - 1);
        }
    }

    public List<String> restoreIpAddresses(String s) {
        List<String> result = new ArrayList<>();
        if ("".equals(s) || s.length() < 4 || s.length() > 12) {
            return result;
        }
        List<String> numList = new ArrayList<>();
        findIp(s, numList, result);
        return result;
    }
}

131.分割回文串

验证字符串是否用125.验证回文串的代码

class Solution {
    /**
     * 参考LeetCode125.验证回文串
     */
    public boolean isPalindrome(String s) {
        s = s.toLowerCase();
        int l = 0;
        int r = s.length() - 1;
        while (l < r) {
            // 只要还没相遇就接着往下走
            if (!Character.isLetterOrDigit(s.charAt(l))) {
                // 左边的字符不是字母或数字
                l++;
                continue;
            }
            if (!Character.isLetterOrDigit(s.charAt(r))) {
                // 右边的字符不是字母或数字
                r--;
                continue;
            }
            // 左右两边都是字母或数字,只要不相等就说明不是回文
            if (s.charAt(l) != s.charAt(r)) {
                return false;
            }
            // 相等地话,继续向中间靠拢
            l++;
            r--;
        }
        return true;
    }

    /**
     * 获取本次循环中的回文串
     *
     * @param s           原始字符串
     * @param palindromes 本次循环中的回文串
     * @param result      存储所有回文串列表的列表
     */
    void getAllPalindromes(String s, List<String> palindromes, List<List<String>> result) {
        if ("".equals(s)) {
            // 当到头时,把回文串列表加入到result中并返回,列表是引用传值,所以必须new一个list
            result.add(new ArrayList<>(palindromes));
            return;
        }
        for (int i = 1; i <= s.length(); i++) {
            String tmp = s.substring(0, i);
            if (!isPalindrome(tmp)) {
                // 不是回文串就直接退出本层递归
                continue;
            }
            palindromes.add(tmp);
            getAllPalindromes(s.substring(i), palindromes, result);
            // 退出本层递归,需要移除一个回文串
            palindromes.remove(palindromes.size() - 1);
        }
    }

    public List<List<String>> partition(String s) {
        List<List<String>> result = new ArrayList<>();
        List<String> palindromes = new ArrayList<>();
        getAllPalindromes(s, palindromes, result);
        return result;
    }
}

3 组合

用基于递归的回溯法解决组合问题

46.全排列

class Solution {
    private void calPermutations(List<Integer> numList, List<Integer> curList, List<List<Integer>> result) {
        if (numList.size() == 0) {
            result.add(new ArrayList<>(curList));
            return;
        }
        for (int i = 0; i < numList.size(); i++) {
            curList.add(numList.get(i));
            List<Integer> numListNext = new ArrayList<>(numList);
            numListNext.remove(i);
            calPermutations(numListNext, curList, result);
            // 递归退出就删除一个元素
            curList.remove(curList.size() - 1);
        }
    }

    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        List<Integer> numList = new ArrayList<>();
        for (int num : nums) {
            numList.add(num);
        }
        calPermutations(numList, curList, result);
        return result;
    }
}

47.全排列II

只需要在上面46题的递归退出逻辑上加一句!result.contains(curList)即可

class Solution {
    private void calPermutations(List<Integer> numList, List<Integer> curList, List<List<Integer>> result) {
        if (numList.size() == 0 && !result.contains(curList)) {
            result.add(new ArrayList<>(curList));
            return;
        }
        for (int i = 0; i < numList.size(); i++) {
            curList.add(numList.get(i));
            List<Integer> numListNext = new ArrayList<>(numList);
            numListNext.remove(i);
            calPermutations(numListNext, curList, result);
            // 递归退出就删除一个元素
            curList.remove(curList.size() - 1);
        }
    }

    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        List<Integer> numList = new ArrayList<>();
        for (int num : nums) {
            numList.add(num);
        }
        calPermutations(numList, curList, result);
        return result;
    }
}

4~5 组合问题

利用递归回溯法解决组合问题.

77.组合

和46、47差不多,简单适配下即可,通过变化索引的方式去遍历子数组要比新建一个子数组效率高很多

class Solution {
    private void calCombinations(int[] nums, int start, List<Integer> curList, List<List<Integer>> result, int k) {
        if (curList.size() == k) {
            result.add(new ArrayList<>(curList));
            return;
        }
        for (int i = start; i < nums.length; i++) {
            curList.add(nums[i]);
            calCombinations(nums,i+1, curList, result, k);
            // 递归退出就删除一个元素
            curList.remove(curList.size() - 1);
        }
    }

    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = i+1;
        }
        calCombinations(nums, 0, curList, result, k);
        return result;
    }
}

39.组合总和

class Solution {
    private void calCombinations(int[] nums, List<Integer> curList, int start, int target, List<List<Integer>> result) {
        if (target < 0) {
            return;
        }
        if (target == 0) {
            result.add(new ArrayList<>(curList));
            return;
        }
        // 从start开始是为了能重复使用start位置的元素
        for (int i = start; i < nums.length; i++) {
            curList.add(nums[i]);
            // 下一层递归还是用这些元素,通过索引来遍历子数组而不要额外建立空间
            calCombinations(nums, curList, i, target - nums[i], result);
            // 递归退出就删除一个元素
            curList.remove(curList.size() - 1);
        }
    }

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 排序后输出结果比较统一
        Arrays.sort(candidates);
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        calCombinations(candidates, curList, 0, target, result);
        return result;
    }
}

40.组合总和 II

和39类似,就是把i换成i+1,然后再添加到result前判重一下即可

class Solution {
    private void calCombinations(int[] nums, List<Integer> curList, int start, int target, List<List<Integer>> result) {
        if (target < 0) {
            return;
        }
        if (target == 0) {
            if(!result.contains(curList)){
                result.add(new ArrayList<>(curList));
            }
            return;
        }
        // 从start开始是为了能重复使用start位置的元素
        for (int i = start; i < nums.length; i++) {
            curList.add(nums[i]);
            // 下一层递归还是用这些元素,通过索引来遍历子数组而不要额外建立空间
            calCombinations(nums, curList, i + 1, target - nums[i], result);
            // 递归退出就删除一个元素
            curList.remove(curList.size() - 1);
        }
    }

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 排序后输出结果比较统一
        Arrays.sort(candidates);
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        calCombinations(candidates, curList, 0, target, result);
        return result;
    }
}

216.组合总和 III

和40题类似。执行用时 : 1 ms , 在所有 Java 提交中击败了 99.29% 的用户

class Solution {
    private void calCombinations(int[] nums, List<Integer> curList, int start, int target, List<List<Integer>> result, int k) {
        if (target < 0) {
            return;
        }
        if (target == 0) {
            if(curList.size() == k){
                result.add(new ArrayList<>(curList));
            }
            return;
        }
        // 从start开始是为了能重复使用start位置的元素
        for (int i = start; i < nums.length; i++) {
            curList.add(nums[i]);
            // 下一层递归还是用这些元素,通过索引来遍历子数组而不要额外建立空间
            calCombinations(nums, curList, i + 1, target - nums[i], result, k);
            // 递归退出就删除一个元素
            curList.remove(curList.size() - 1);
        }
    }

    public List<List<Integer>> combinationSum3(int k, int n) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        int[] nums = {1,2,3,4,5,6,7,8,9};
        calCombinations(nums, curList, 0, n, result, k);
        return result;
    }
}

78.子集

class Solution {
    private void calSubsets(int[] nums, int start, List<Integer> curList, List<List<Integer>> result){
        if(start == nums.length){
            return;
        }
        for(int i = start; i < nums.length; i++){
            curList.add(nums[i]);
            if(!result.contains(curList)){
                result.add(new ArrayList<>(curList));
            }
            calSubsets(nums, i + 1, curList, result);
            curList.remove(curList.size() - 1);
        }
    }
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        result.add(new ArrayList<>());
        calSubsets(nums, 0, curList, result);
        return result;
    }
}

90.子集II

和上一个题相比就加了个Arrays.sort(nums);

class Solution {
    private void calSubsets(int[] nums, int start, List<Integer> curList, List<List<Integer>> result){
        if(start == nums.length){
            return;
        }
        for(int i = start; i < nums.length; i++){
            curList.add(nums[i]);
            if(!result.contains(curList)){
                result.add(new ArrayList<>(curList));
            }
            calSubsets(nums, i + 1, curList, result);
            curList.remove(curList.size() - 1);
        }
    }
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        result.add(new ArrayList<>());
        calSubsets(nums, 0, curList, result);
        return result;
    }
}

401.二进制手表

class Solution {
    private void getResult(int[] nums, int start, int k, List<Integer> curList, List<List<Integer>> result){
        if(curList.size() == k){
            result.add(new ArrayList(curList));
            return;
        }
        for(int i = start; i < nums.length; i++){
            curList.add(nums[i]);
            getResult(nums, i + 1, k, curList, result);
            curList.remove(curList.size() - 1);
        }
    }
    private List<String> trans(List<List<Integer>> result){
        List<String> list = new ArrayList<>();
        for(List<Integer> lightList : result){
            int[] flags = new int[10];
            for(int i = 0; i < 10; i++){
                if(lightList.contains(i)){
                    flags[i] = 1;
                }else {
                    flags[i] = 0;
                }
            }
            int hour = 8 * flags[0] + 4 * flags[1] + 2 * flags[2] + flags[3];
            int minute = 32 * flags[4] + 16 * flags[5] + 8 * flags[6] + 4 * flags[7] + 2 * flags[8] + flags[9];
            if(hour > 11 || minute > 59){
                continue;
            }
            if(minute < 10){
                list.add(hour + ":0" + minute);
            }else{
                list.add(hour + ":" + minute);
            } 
        }
        Collections.sort(list);
        return list;
    }
    // 组合问题
    public List<String> readBinaryWatch(int num) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> curList = new ArrayList<>();
        // 下标0~3的元素表示上面四个灯,下标4~9的元素表示下面6个灯
        int[] nums = {0,1,2,3,4,5,6,7,8,9};
        getResult(nums, 0, num, curList, result);
        return trans(result);
    }
}

6 二维平面的搜索

参考Part2BasicGraph/第06章_图论问题建模和floodfill,这里是floodfill的四连通分量问题,可以用DFS解决,在递归回退时记得要把相关状态重置下

79.单词搜索

class Solution {
    /**
     * 当前点求上右下左四个点时用到的矢量差,dirs 是directions的意思
     */
    private int[][] dirs = {
            {-1, 0},
            {0, 1},
            {1, 0},
            {0, -1}
    };

    /**
     * 传进来的grid有多少行(Row)多少列(Col)
     */
    private int R, C;

    /**
     * 岛屿网格的情况
     */
    private char[][] grid;
    private boolean[][] visited;

    /**
     * 判断第x行第y列的点(x, y)是否在grid所在的范围内
     */
    private boolean inArea(int x, int y) {
        return x >= 0 && x < R && y >= 0 && y < C;
    }

    /**
     * 图的初始化
     *
     * @param board 二维的图
     * @param word  要找的单词
     * @return
     */
    public boolean exist(char[][] board, String word) {
        this.grid = board;
        if (grid == null) {
            return false;
        }
        R = grid.length;
        if (R == 0) {
            // 如果行数为0说明图为空,直接返回
            return false;
        }
        C = grid[0].length;
        if (C == 0) {
            // 如果列数为0说明图为空,直接返回
            return false;
        }
        visited = new boolean[R][C];
        for (int i = 0; i < R; i++) {
            for (int j = 0; j < C; j++) {
                // 点没有被访问而且字符等于word的第一个字符,才以这个点作为起点进行DFS遍历
                if (grid[i][j] == word.charAt(0)) {
                    // 以(i,j)作为起点进行DFS,看看遍历结果能否组成提供的单词
                    List<Character> cList = new ArrayList<>();
                    cList.add(word.charAt(0));
                    dfs(i, j, 1, word, cList);
                    if (cList.size() == word.length()) {
                        return true;
                    }
                    // 每次递归DFS遍历完,数组要重新初始化
                    visited = new boolean[R][C];
                }
            }
        }
        return false;
    }

    private void dfs(int x, int y, int start, String word, List<Character> cList) {
        if (cList.size() == word.length()) {
            return;
        }
        visited[x][y] = true;
        // 遍历节点(x, y)所有的邻接点,判断是陆地的
        for (int d = 0; d < dirs.length; d++) {
            int nextX = x + dirs[d][0];
            int nextY = y + dirs[d][1];
            // 点(next_x, next_y)必须在grid区域内 + 没被访问过 + 是陆地(点(x, y)是陆地)
            if (inArea(nextX, nextY) && !visited[nextX][nextY] && grid[nextX][nextY] == word.charAt(start)) {
                cList.add(grid[nextX][nextY]);
                dfs(nextX, nextY, start + 1, word, cList);
                if (cList.size() == word.length()) {
                    break;
                }
                // 状态重置:列表和访问数组
                cList.remove(cList.size() - 1);
                visited[nextX][nextY] = false;
            }
        }
    }

    /**
     * char[][] board = {{'A', 'B', 'C', 'E'}, {'S', 'F', 'C', 'S'}, {'A', 'D', 'E', 'E'}};
     * String word = "ABCCED";
     * <p>
     * char[][] board = {{'C', 'A', 'A'}, {'A', 'A', 'A'}, {'B', 'C', 'D'}};
     * String word = "AAB";
     * <p>
     * char[][] board = {{'A','B','C','E'},{'S','F','E','S'},{'A','D','E','E'}};
     * String word  = "ABCEFSADEESE";
     */
    public static void main(String[] args) {
        char[][] board = {{'C', 'A', 'A'}, {'A', 'A', 'A'}, {'B', 'C', 'D'}};
        String word = "AAB";
        System.out.println(new Solution().exist(board, word));
    }
}

7 floodfill(实际就是DFS)

二维图的搜索实际就是DFS

200.岛屿数量

实际就是DFS求连通分量的个数

public class Solution {
    /**
     * 当前点求上右下左四个点时用到的矢量差,dirs 是directions的意思
     */
    private int[][] dirs = {
            {-1, 0},
            {0, 1},
            {1, 0},
            {0, -1}
    };

    /**
     * 传进来的grid有多少行(Row)多少列(Col)
     */
    private int R, C;

    /**
     * 岛屿网格的情况
     */
    private char[][] grid;
    private boolean[][] visited;
    /**
     * 连通分量的个数
     */
    private int cCount = 0;

    /**
     * 判断第x行第y列的点(x, y)是否在grid所在的范围内
     */
    private boolean inArea(int x, int y) {
        return x >= 0 && x < R && y >= 0 && y < C;
    }

    public int numIslands(char[][] grid) {
        this.grid = grid;
        if (grid == null) {
            return 0;
        }
        R = grid.length;
        if (R == 0) {
            // 如果行数为0说明图为空,直接返回
            return 0;
        }
        C = grid[0].length;
        if (C == 0) {
            // 如果列数为0说明图为空,直接返回
            return 0;
        }
        visited = new boolean[R][C];
        for (int i = 0; i < R; i++) {
            for (int j = 0; j < C; j++) {
                // 点没有被访问而且字符等于word的第一个字符,才以这个点作为起点进行DFS遍历
                if (grid[i][j] == '1' && !visited[i][j]) {
                    dfs(i, j);
                    cCount++;
                }
            }
        }
        return cCount;
    }

    private void dfs(int x, int y) {
        visited[x][y] = true;
        // 遍历节点(x, y)所有的邻接点,判断是陆地的
        for (int d = 0; d < dirs.length; d++) {
            int nextX = x + dirs[d][0];
            int nextY = y + dirs[d][1];
            // 点(next_x, next_y)必须在grid区域内 + 没被访问过 + 是陆地(点(x, y)是陆地)
            if (inArea(nextX, nextY) && !visited[nextX][nextY] && grid[nextX][nextY] == '1') {
                dfs(nextX, nextY);
            }
        }
    }
}

139.被围绕的区域

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] visited = new boolean[s.length() + 1];
        return dfs(s, 0, wordDict, visited);
    }

    private boolean dfs(String s, int start, List<String> wordDict, boolean[] visited) {
        visited[start] = true;
        for (int i = start; i < s.length(); i++) {
            // 防止递归回退的时候再次经过这个字符
            if (visited[i + 1]) {
                continue;
            }
            String substr = s.substring(start, i + 1);
            if (wordDict.contains(substr)) {
                if (i + 1 == s.length() || dfs(s, i + 1, wordDict, visited)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 输入: s = "leetcode", wordDict = ["leet", "code"]  输出: true
     * <p>
     * 输入:"aaaaaaa"  输出:["aaaa","aaa"]
     */
    public static void main(String[] args) {
        String s = "aaaaaaa";
        String[] words = {"aaaa", "aaa"};
        List<String> wordDict = new ArrayList<>(Arrays.asList(words));
        System.out.println(new Solution().wordBreak(s, wordDict));
    }
}

417.太平洋大西洋水流问题

遍历每个点,DFS找到既能到达太平洋又能到达大西洋的点加入到结果中即可

class Solution {
    /**
     * 当前点求上右下左四个点时用到的矢量差,dirs 是directions的意思
     */
    private int[][] dirs = {
            {-1, 0},
            {0, 1},
            {1, 0},
            {0, -1}
    };

    /**
     * 传进来的grid有多少行(Row)多少列(Col)
     */
    private int R, C;

    /**
     * 岛屿网格的情况
     */
    private int[][] grid;
    private boolean[][] visited;

    /**
     * 判断第x行第y列的点(x, y)是否在grid所在的范围内
     */
    private boolean inArea(int x, int y) {
        return x >= 0 && x < R && y >= 0 && y < C;
    }

    public List<List<Integer>> pacificAtlantic(int[][] matrix) {
        List<List<Integer>> result = new ArrayList<>();
        this.grid = matrix;
        if (grid == null) {
            return result;
        }
        R = grid.length;
        if (R == 0) {
            // 如果行数为0说明图为空,直接返回
            return result;
        }
        C = grid[0].length;
        if (C == 0) {
            // 如果列数为0说明图为空,直接返回
            return result;
        }
        visited = new boolean[R][C];
        for (int i = 0; i < R; i++) {
            for (int j = 0; j < C; j++) {
                // 点没有被访问而且字符等于word的第一个字符,才以这个点作为起点进行DFS遍历
                boolean pacific = dfs(i, j, true);
                visited = new boolean[R][C];
                boolean atlantic = dfs(i, j, false);
                visited = new boolean[R][C];
                if (pacific && atlantic) {
                    List<Integer> list = new ArrayList<>();
                    list.add(i);
                    list.add(j);
                    result.add(list);
                }
            }
        }
        return result;
    }

    /**
     * dfs遍历看起点是否能到到
     *
     * @param x       行
     * @param y       列
     * @param pacific true代表检查能否到达太平洋(),false代表检查能否到到大西洋
     * @return 是否能到达太平洋或大西洋
     */
    private boolean dfs(int x, int y, boolean pacific) {
        if (pacific) {
            // 检查能否到达太平洋
            if (x == 0 || y == 0) {
                return true;
            }
        } else {
            // 检查大西洋
            if (x == R - 1 || y == C - 1) {
                return true;
            }
        }
        visited[x][y] = true;
        for (int d = 0; d < dirs.length; d++) {
            int nextX = x + dirs[d][0];
            int nextY = y + dirs[d][1];
            // 点(next_x, next_y)必须在grid区域内 + 没被访问过 + 往地处流 + 能到达边界
            if (inArea(nextX, nextY) && !visited[nextX][nextY] && grid[nextX][nextY] <= grid[x][y] && dfs(nextX, nextY, pacific)) {
                return true;
            }
        }
        // 到最后都没能到达边界,则说明无法到到,退出即可
        return false;
    }

    public static void main(String[] args) {
        int[][] matrix = {
                {1, 2, 2, 3, 5},
                {3, 2, 3, 4, 4},
                {2, 4, 5, 3, 1},
                {6, 7, 1, 4, 5},
                {5, 1, 1, 2, 4}
        };
        System.out.println(new Solution().pacificAtlantic(matrix)); 
    }
}

8 递归回溯法是人工智能的基础

51.N皇后问题

图中的i是下面的r,

  • N皇后问题1
  • N皇后问题2
  • N皇后问题3
class Solution {
    boolean[] col = null;
    boolean[] left = null;
    boolean[] right = null;
    List<List<String>> ret = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        // col[i]表示第i列被占用
        col = new boolean[n];
        // 左倾对角线
        left = new boolean[2 * n - 1];
        // 右倾对角线
        right = new boolean[2 * n - 1];
        // 二维平面
        char[][] board = new char[n][n];
        // 回溯法求解问题
        backTrack(board, 0, n);
        return ret;
    }
    // i代表行,j代表列
    void backTrack(char[][] board, int i, int n) {
        if (i >= n) {
            // 找到能一个能摆放皇后的位置,加入到结果列表中
            List<String> list = new ArrayList<>();
            for (int j = 0; j < n; j++) {
                list.add(new String(board[j]));
            }
            ret.add(list);
            return;
        }
        // 初始化棋盘啥都不放
        Arrays.fill(board[i], '.');
        for (int j = 0; j < n; j++) {
            if (!col[j] && !left[i + j] && !right[i - j + n - 1]) {
                // 一、尝试在当前位置摆放皇后
                // 摆放皇后
                board[i][j] = 'Q';
                // 列占用
                col[j] = true;
                // 左倾对角线的规律
                left[i + j] = true;
                // 右倾对角线的规律
                right[i - j + n - 1] = true;
                // 二、递归求解其合法性
                backTrack(board, i + 1, n);
                // 三、递归回退过程中需要恢复初始的状态
                // 拿走皇后
                board[i][j] = '.';
                // 列占用解除
                col[j] = false;
                // 左对角线占用解除
                left[i + j] = false;
                // 右对角线占用解除
                right[i - j + n - 1] = false;
            }
        }
    }
}

类似问题还有52和37

52.N皇后II

把51返回的ret改成ret.size()即可

class Solution {
    boolean[] col = null;
    boolean[] left = null;
    boolean[] right = null;
    List<List<String>> ret = new ArrayList<>();

    public int totalNQueens(int n) {
        // col[i]表示第i列被占用
        col = new boolean[n];
        // 左倾对角线
        left = new boolean[2 * n - 1];
        // 右倾对角线
        right = new boolean[2 * n - 1];
        // 二维平面
        char[][] board = new char[n][n];
        // 回溯法求解问题
        backTrack(board, 0, n);
        // 和51题唯一不同的地方
        return ret.size();
    }
    // i代表行,j代表列
    void backTrack(char[][] board, int i, int n) {
        if (i >= n) {
            // 找到能一个能摆放皇后的位置,加入到结果列表中
            List<String> list = new ArrayList<>();
            for (int j = 0; j < n; j++) {
                list.add(new String(board[j]));
            }
            ret.add(list);
            return;
        }
        // 初始化棋盘啥都不放
        Arrays.fill(board[i], '.');
        for (int j = 0; j < n; j++) {
            if (!col[j] && !left[i + j] && !right[i - j + n - 1]) {
                // 一、尝试在当前位置摆放皇后
                // 摆放皇后
                board[i][j] = 'Q';
                // 列占用
                col[j] = true;
                // 左倾对角线的规律
                left[i + j] = true;
                // 右倾对角线的规律
                right[i - j + n - 1] = true;
                // 二、递归求解其合法性
                backTrack(board, i + 1, n);
                // 三、递归回退过程中需要恢复初始的状态
                // 拿走皇后
                board[i][j] = '.';
                // 列占用解除
                col[j] = false;
                // 左对角线占用解除
                left[i + j] = false;
                // 右对角线占用解除
                right[i - j + n - 1] = false;
            }
        }
    }
}

37.解数独

class Solution {
    final int N = 9;
    int[] row = new int [N], col = new int [N];
    //ones数组表示0~2^9 - 1的整数中二进制表示中1的个数:如ones[7] = 3 ones[8] = 1
    //map数组表示2的整数次幂中二进制1所在位置(从0开始) 如 map[1] = 0,map[2] = 1, map[4] = 2
    int[] ones = new int[1 << N], map = new int[1 << N];
    int[][] cell = new int[3][3]; 
    public void solveSudoku(char[][] board) {
        init();
        int cnt = fill_state(board);
        dfs(cnt, board);
    }
    void init(){
        for(int i = 0; i < N; i++) row[i] = col[i] = (1 << N) - 1; 
        for(int i = 0; i < 3; i++)
            for(int j = 0; j < 3; j++)
                cell[i][j] = (1 << N) - 1;
        //以上2个循环把数组的数初始化为二进制表示9个1,即一开始所以格子都可填
        for(int i = 0; i < N; i++) map[1 << i] = i;
        //统计0~2^9 - 1的整数中二进制表示中1的个数
        for(int i = 0; i < 1 << N; i++){
            int n = 0;
            for(int j = i; j != 0; j ^= lowBit(j)) n++;
            ones[i] = n;
        }
    }
    int fill_state(char[][] board){
        int cnt = 0;    //统计board数组空格('.')的个数
        for(int i = 0; i < N; i++){
            for(int j = 0; j < N; j++){
                if(board[i][j] != '.'){
                    int t = board[i][j] - '1';
                    //数独中 i,j位置为数组下标,修改row col cell数组中状态
                    change_state(i, j, t);  
                }else cnt++;
            }
        }
        return cnt;
    }
    boolean dfs(int cnt, char[][] board){
        if(cnt == 0) return true;
        int min = 10, x = 0, y = 0;
        //剪枝,即找出当前所以空格可填数字个数最少的位置 记为x y
        for(int i = 0; i < N; i++){
            for(int j = 0; j < N; j++){
                if(board[i][j] == '.'){
                    int k = ones[get(i, j)];
                    if(k < min){
                        min = k;
                        x = i;
                        y = j;
                    }
                }
            }
        }
        //遍历当前 x y所有可选数字
        for(int i = get(x, y); i != 0; i ^= lowBit(i)){
            int t = map[lowBit(i)];
            
            change_state(x, y, t);
            board[x][y] = (char)('1' + t);
            
            if(dfs(cnt - 1, board)) return true;
            
            //恢复现场
            change_state(x, y, t);
            board[x][y] = '.';
        }
        return false;
    }
    void change_state(int x, int y, int t){
        row[x] ^= 1 << t;
        col[y] ^= 1 << t;
        cell[x / 3][y / 3] ^= 1 << t;
    }
    //对维护数组x行,y列的3个集合(行、列、九宫格)进行&运算
    //就可以获得可选数字的集合(因为我们用1标识可填数字)
    int get(int x, int y){
        return row[x] & col[y] & cell[x / 3][y / 3];
    }
    int lowBit(int x){
        return -x & x;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/439617.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Netty使用Google Protobuf进行编解码

文章目录 一、概述1、编解码基础2、Netty编解码器3、Protobuf概述 二、Protobuf基本使用1、引入jar包2、下载Protobuf3、编写Student.proto4、生成StudentPOJO类5、服务器端6、客户端7、验证一下吧 三、Netty使用Protobuf发送多类型对象1、编写Student.proto2、生成MyDataInfo.…

测试基础概念常见测试开发模型

文章目录&#xff1a;一.什么是需求&#xff08;1&#xff09;用户需求 &#xff08;2&#xff09;软件需求 二.测试用例 &#xff08;1&#xff09;测试用例的含义 &#xff08;2&#xff09;测试用例的作用 三.开发模型和测试模型&#xff08;1&#xff09;软件生命周期…

2023MathorcupC题电商物流网络包裹应急调运与结构优化问题建模详解+模型代码(一)

电商物流网络包裹应急调运与结构优化问题 第三次继续写数模文章和思路代码了,不知道上次美赛和国赛大家有没有认识我,没关系今年只要有数模比赛艾特我私信我,要是我有时间我一定免费出文章代码好吧!博主参与过十余次数学建模大赛,三次美赛获得过二次M奖一次H奖,国赛二等…

MySQL:JDBC 详细内容

文章目录 Day 04&#xff1a;一、JDBC1. 数据库驱动2. 概述3. 第一个 JDBC 程序4. JDBC 中对象的解释 二、改进 JDBC 程序1. 思路2. 实践注意点3. 分析4. 结果5. 代码 三、SQL 注入问题四、PreparedStatement 对象1. 实践注意点2. 分析&#xff08;增、删、改、查&#xff09;3…

电脑能录屏吗?当然可以!看看这3种方法!

案例&#xff1a;电脑有录屏功能吗&#xff1f; “我的客户让我发一个项目展示的视频&#xff0c;完成这个任务需要对电脑进行录制。问题是&#xff0c;台式电脑有录屏功能吗&#xff1f;笔记本电脑有录屏功能吗&#xff1f;电脑能录屏吗&#xff1f;有没有好心人解答一下我的…

一遍讲清楚:偏向锁到轻量级锁的升级过程(为什么耗资源)

目录 上原理&#xff1a; 细说原理&#xff1a; 什么是锁记录呢&#xff1f; 什么是Mark Word 呢&#xff1f; 上图解&#xff1a; 上原理&#xff1a; 偏向锁使⽤了⼀种等到竞争出现才释放锁的机制&#xff0c;所以当其他线程尝试竞争偏向锁时&#xff0c; 持有偏向锁的…

Java基础——IO流+字节/字符流使用

IO流 &#xff08;1&#xff09;IO流的概述&#xff1a; IO流也称为输入&#xff0c;输出流&#xff0c;就是用来读写数据的。I表示input&#xff0c;是数据从硬盘文件读入到内存的过程&#xff0c;称之输入&#xff0c;负责读。O表示output&#xff0c;是内存程序的数据从内…

【socket通信】python实现简单socket通信| server和client

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、socket通信基础知识1.1基础知识1.2socket类型 二、socket python实现2.1.服务器代码 server.py2.2.客户端代码 client.py2.3.如何运行 补充的计网知识 前言…

2023年TikTok网红营销:从短视频到直播,多维度提升品牌价值

随着TikTok的持续热度&#xff0c;TikTok网红营销已经成为了品牌推广中不可忽视的一部分。在2023年&#xff0c;TikTok网红营销将会继续保持强劲的发展势头。本文Nox聚星将会详细介绍2023年TikTok网红营销的发展趋势&#xff0c;并探讨品牌应该如何抓住这些趋势来提高自己的推广…

「 JavaSE 」说说什么是泛型的类型擦除?

「 JavaSE 」说说什么是泛型的类型擦除&#xff1f; 参考&鸣谢 面试官&#xff1a;说说什么是泛型的类型擦除&#xff1f; Dr Hydra Java泛型类型擦除以及类型擦除带来的问题 蜗牛大师 文章目录 「 JavaSE 」说说什么是泛型的类型擦除&#xff1f;一、前言二、类型擦除做了…

Windows应急响应排查思路

「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 Windows应急响应 一、用户分析1、普通用户2、隐藏用户3、克隆账户 二、日志分析1、Window…

基于互相关性的信号同步

许多测量涉及多个传感器异步采集的数据。如果您要集成信号并以关联式研究它们&#xff0c;您必须同步它们。为此&#xff0c;请使用 xcorr。 例如&#xff0c;假设有一辆汽车经过一座桥。它产生的振动由位于不同位置的三个相同传感器进行测量。信号有不同到达时间。 将三个时…

【技术分享】Livedata粘性事件实现源码解析,让你彻底掌握数据更新机制

概述 Livedata粘性事件是Android中常用的一种观察者模式&#xff0c;它可以让数据在发生改变时通知观察者并更新UI。在实际开发中&#xff0c;我们可能会遇到粘性事件的情况&#xff0c;即先发送了一个数据&#xff0c;后来才有观察者来监听&#xff0c;这时候我们需要保证观察…

集合Map高频面试题

1、介绍下 HashMap 的底层数据结构吧。 在 JDK 1.8&#xff0c;HashMap 底层是由 “数组链表红黑树” 组成&#xff0c;如下图所示&#xff0c;而在 JDK 1.8 之前是由 “数组链表” 组成&#xff0c;就是下图去掉红黑树。 2、为什么使用“数组链表”&#xff1f; 使用 “数组…

如何用 ModelScope 实现 “AI 换脸” 视频

前言 当下&#xff0c;视频内容火爆&#xff0c;带有争议性或反差大的换脸视频总能吸引人视线。虽然 AI 换脸在市面上已经流行了许久&#xff0c;相关制作工具或移动应用也是数不胜数。但是多数制作工具多数情况下不是会员就是收费&#xff0c;而且替换模板有限。以下在实战的角…

三电技术之电机电驱技术

三电技术之电机电驱技术 1 基本功能 电动汽车驱动电机及其控制系统是电动汽车的心脏&#xff0c;是把电能转化为机械能来驱动车辆的部件。它的任务是在驾驶人的控制下&#xff0c;高效率地将动力电池的能量转化为车轮的动能&#xff0c;或者将车轮上的动能反馈到动力电池中。 …

16 个优秀的 Vue 开源项目

为什么我们要关注Vue Vue是一个用于构建用户界面的JavaScript框架。值得关注的是&#xff0c;它在没有谷歌和Facebook的支持下获得了大量的人气。 Vue是结合react和angular的最好的方法&#xff0c;并且拥有一个有凝聚力的&#xff0c;活跃的&#xff0c;能够应对开发问题的大型…

消息队列如何保证消息的幂等性

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 文章目录 什么是幂等性什么是消息的幂等性为什么会出现消息幂等性问题该如何解决消息幂等性问题总结 在分布式系统中&#xff0c;消息队列…

【C++入门必备知识:命名空间与关键字】

【C入门必备知识&#xff1a;命名空间与关键字】 【命名空间】①.命名空间定义Ⅰ.正常定义命名空间Ⅱ.嵌套定义命名空间Ⅲ.合并命名空间 ②.命名空间的使用Ⅰ.命名空间名称及域作用限定符Ⅱ.using成员引入Ⅲ.using namespace名称全部引入 ③.注意事项 【C关键字(C98)】 【命名空…

Ubuntu系统安装docker、docker-compse

环境&#xff1a;Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-100-generic x86_64) 一、安装docker 1.卸载旧版本 ubuntu下自带了docker的库&#xff0c;不需要添加新的源。 但是ubuntu自带的docker版本太低&#xff0c;需要先卸载旧的再安装新的 sudo apt-get remove docker docke…