7、递归和回溯法

内容来自刘宇波老师玩转算法面试

回溯算法解题套路框架
所有方法二实现思路:选择列表(排除不合法的选择 + 做选择 + 进入下一层回溯树 + 取消选择)、路径、结束条件
1、可以把「选择列表」和「路径」作为决策树的每个节点的属性
2、我们定义的「backtrack」函数其实就像一个指针,在这棵树上游走,同时要正确维护每个节点的属性
3、每当走到树的底层叶子节点,其「路径」就是一个解

回溯算法秒杀所有排列 - 组合 - 子集问题
排列有序、组合无序:比如从 1 - 8 号球取 3 个球
1、排列:如果说一个一个拿,拿出来依次是 3 号、2 号、4 号,那么你拿出 234 和 324 是不一样的,这就是排列,有序的
2、组合:如果任意取 3 个球,由于 3 个一起取出,比如你取出的是 123 号球,不存在 123 和 321 有区别,都是这三个,这就是组合,无序的

一文秒杀所有岛屿题目

1、什么是回溯

image

更多问题
93 - 复原 IP 地址
131 - 分割回文串

2、树形问题

17 - 电话号码的字母组合

image
image

2.1、方法一

public class Solution {

    private static final String[] letterMap = {" ", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    private static final List<String> res = new ArrayList<>();

    public static List<String> letterCombinations(String digits) {
        res.clear();
        if (digits == null || digits.equals("")) return res;

        findCombination(digits, 0, "");
        return res;
    }

    /**
     * s 中保存了 digits[0 ... index - 1] 翻译得到的一个字母字符串
     * 寻找和 digits[index] 匹配的字母, 获得 digits[0 ... index] 翻译得到的解
     */
    private static void findCombination(String digits, int index, String s) {
        if (index == digits.length()) {
            res.add(s);
            return;
        }

        char c = digits.charAt(index);
        String letters = letterMap[c - '0'];
        for (int i = 0; i < letters.length(); i++) {
            findCombination(digits, index + 1, s + letters.charAt(i));
        }
    }
}

2.2、方法二

public class Solution {

    private static final String[] letterMap = {" ", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    private static final List<String> res = new LinkedList<>();

    public static List<String> letterCombinations(String digits) {
        res.clear();
        if (digits == null || digits.equals("")) return res;

        StringBuilder track = new StringBuilder(); // 路径
        backtrack(digits, 0, track);
        return res;
    }

    /**
     * 回溯算法框架
     */
    private static void backtrack(String digits, int index, StringBuilder track) {
        // 到达叶子节点, 将路径装入结果列表
        if (track.length() == digits.length()) {
            res.add(track.toString());
            return;
        }

        char c = digits.charAt(index);
        String letters = letterMap[c - '0'];
        for (int i = 0; i < letters.length(); i++) {
            track.append(letters.charAt(i));        // 做选择
            backtrack(digits, index + 1, track);    // 进入下一层回溯树
            track.deleteCharAt(track.length() - 1); // 取消选择
        }
    }
}

3、回溯法是经典人工智能的基础

51 - N 皇后

image

3.1、图示

image
image
image
image

3.2、实现

public class Solution {

    private static boolean[] col;  // 纵向
    private static boolean[] dia1; // 对角线撇
    private static boolean[] dia2; // 对角线捺
    private static ArrayList<List<String>> res;

    public static List<List<String>> solveNQueens(int n) {
        res = new ArrayList<>();
        col = new boolean[n];          // 纵向
        dia1 = new boolean[2 * n - 1]; // 对角线撇
        dia2 = new boolean[2 * n - 1]; // 对角线捺

        LinkedList<Integer> row = new LinkedList<>(); // 路径
        backtrack(n, 0, row);

        return res;
    }

    /**
     * 尝试在一个 n 皇后问题中, 摆放第 index 行的皇后位置
     */
    private static void backtrack(int n, int index, LinkedList<Integer> row) {
        if (index == n) {
            res.add(generateBoard(n, row));
            return;
        }

        for (int i = 0; i < n; i++) {
            // 尝试将第 index 行的皇后摆放在第 i 列
            if (!col[i] && !dia1[index + i] && !dia2[index - i + n - 1]) {
                // 做选择
                row.addLast(i);
                col[i] = true;
                dia1[index + i] = true;
                dia2[index - i + n - 1] = true;

                // 进入下一层回溯树
                backtrack(n, index + 1, row);

                // 取消选择
                col[i] = false;
                dia1[index + i] = false;
                dia2[index - i + n - 1] = false;
                row.removeLast();
            }
        }
    }

    private static List<String> generateBoard(int n, LinkedList<Integer> row) {
        ArrayList<String> board = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            char[] charArray = new char[n];
            Arrays.fill(charArray, '.');
            charArray[row.get(i)] = 'Q';
            board.add(new String(charArray));
        }
        return board;
    }
}

3.3、更多

52 - N 皇后 II
37 - 解数独

4、排列问题

46 - 全排列

image

更多问题
47 - 全排列 II

4.1、方法一

public class Solution {

    private static final List<List<Integer>> res = new ArrayList<>();
    private static boolean[] used;

    public static List<List<Integer>> permute(int[] nums) {
        res.clear();
        if (nums == null || nums.length == 0) return res;

        used = new boolean[nums.length];
        Arrays.fill(used, false);

        generatePermutation(nums, 0, new ArrayList<>());
        return res;
    }

    /**
     * p 中保存了一个有 index 个元素的排列
     * 向这个排列的末尾添加第 index + 1 个元素, 获得一个有 index + 1 个元素的排列
     */
    private static void generatePermutation(int[] nums, int index, ArrayList<Integer> p) {
        if (index == nums.length) {
            res.add(new ArrayList<>(p));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;

            p.add(nums[i]);
            used[i] = true;
            generatePermutation(nums, index + 1, p);

            p.remove(p.size() - 1);
            used[i] = false;
        }
    }
}

4.2、方法二

回溯算法核心套路详解
回溯算法解题套路框架

public class Solution {

    private static final List<List<Integer>> res = new ArrayList<>();
    private static boolean[] used;

    public static List<List<Integer>> permute(int[] nums) {
        res.clear();
        if (nums == null || nums.length == 0) return res;

        used = new boolean[nums.length];

        LinkedList<Integer> track = new LinkedList<>(); // 路径
        backtrack(nums, track);
        return res;
    }

    /**
     * 回溯算法框架
     */
    private static void backtrack(int[] nums, LinkedList<Integer> track) {
        // 到达叶子节点, 将路径装入结果列表
        if (track.size() == nums.length) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (used[i]) continue;  // 排除不合法的选择

            track.add(nums[i]);     // 做选择
            used[i] = true;
            backtrack(nums, track); // 进入下一层回溯树

            track.removeLast();     // 取消选择
            used[i] = false;
        }
    }
}

5、组合问题

77 - 组合

image

5.1、方法一

public class Solution {

    private static final List<List<Integer>> res = new ArrayList<>();

    public static List<List<Integer>> combine(int n, int k) {
        res.clear();

        LinkedList<Integer> track = new LinkedList<>();
        generateCombinations(n, k, 1, track);
        return res;
    }

    /**
     * 求解 C(n, k), 当前已经找到的组合存储在 track 中, 需要从 start 开始搜索新的元素
     */
    private static void generateCombinations(int n, int k, int start, LinkedList<Integer> track) {
        if (track.size() == k) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = start; i <= n; i++) {
            track.add(i);
            generateCombinations(n, k, i + 1, track);
            track.removeLast();
        }
    }
}

5.2、方法二

回溯算法秒杀所有排列/组合/子集问题
回溯算法秒杀所有排列-组合-子集问题

public class Solution {

    private static final List<List<Integer>> res = new LinkedList<>();
    private static final LinkedList<Integer> track = new LinkedList<>(); // 路径

    public static List<List<Integer>> combine(int n, int k) {
        res.clear();
        track.clear();

        backtrack(1, n, k);
        return res;
    }

    /**
     * 回溯算法框架
     */
    private static void backtrack(int start, int n, int k) {
        // 到达叶子节点, 将路径装入结果列表
        if (k == track.size()) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = start; i <= n; i++) {
            track.addLast(i);       // 做选择
            backtrack(i + 1, n, k); // 进入下一层回溯树
            track.removeLast();     // 取消选择
        }
    }
}

6、组合问题的优化

更多问题
39 - 组合总和
40 - 组合总和 II
216 - 组合总和 III
78 - 子集
90 - 子集 II
401 - 二进制手表

6.1、方法一

public class Solution {

    private static final List<List<Integer>> res = new ArrayList<>();

    public static List<List<Integer>> combine(int n, int k) {
        res.clear();

        LinkedList<Integer> track = new LinkedList<>(); // 路径
        generateCombinations(n, k, 1, track);
        return res;
    }

    /**
     * 求解 C(n, k), 当前已经找到的组合存储在 track 中, 需要从 start 开始搜索新的元素
     */
    private static void generateCombinations(int n, int k, int start, LinkedList<Integer> track) {
        if (track.size() == k) {
            res.add(new LinkedList<>(track));
            return;
        }

        // 还有 k - track.size() 个空位, 所以 [i ... n] 中至少要有 k - track.size() 个元素
        // 因此 i <= n - (k - track.size() - 1)
        for (int i = start; i <= n - (k - track.size() - 1); i++) {
            track.add(i);
            generateCombinations(n, k, i + 1, track);
            track.removeLast();
        }
    }
}

6.2、方法二

public class Solution {

    private static final List<List<Integer>> res = new ArrayList<>();
    private static final LinkedList<Integer> track = new LinkedList<>(); // 路径

    public static List<List<Integer>> combine(int n, int k) {
        res.clear();
        track.clear();

        backtrack(1, n, k);
        return res;
    }

    private static void backtrack(int start, int n, int k) {
        // 到达叶子节点, 将路径装入结果列表
        if (k == track.size()) {
            res.add(new LinkedList<>(track));
            return;
        }

        // 还有 k - track.size() 个空位, 所以 [i ... n] 中至少要有 k - track.size() 个元素
        // 因此 i <= n - (k - track.size() - 1)
        for (int i = start; i <= n - (k - track.size() - 1); i++) {
            track.addLast(i);       // 做选择
            backtrack(i + 1, n, k); // 进入下一层回溯树
            track.removeLast();     // 取消选择
        }
    }
}

7、floodfill 算法

200 - 岛屿数量

image

public class Solution {

    private static int dir[][] = {
            {0, 1}, {1, 0}, {0, -1}, {-1, 0}
    };
    private static int m, n; // m - 1 行 n - 1 列
    private static boolean visited[][];

    public static int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;

        m = grid.length;
        n = grid[0].length;
        visited = new boolean[m][n];

        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1' && !visited[i][j]) {
                    res++;
                    dfs(grid, i, j);
                }
            }
        }

        return res;
    }

    /**
     * 从 grid[x][y] 的位置开始, 进行 floodfill
     * 保证 (x, y) 合法, 且 grid[x][y] 是没有被访问过的陆地
     */
    private static void dfs(char[][] grid, int x, int y) {
        visited[x][y] = true;
        for (int i = 0; i < 4; i++) {
            int newx = x + dir[i][0];
            int newy = y + dir[i][1];
            if (inArea(newx, newy) && !visited[newx][newy] && grid[newx][newy] == '1') dfs(grid, newx, newy);
        }
    }

    private static boolean inArea(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}

更多问题
130 - 被围绕的区域
417 - 太平洋大西洋水流问题

8、二维平面上的回溯法

79 - 单词搜索

image

public class Solution {

    // 上右下左
    private static int dir[][] = {
            {-1, 0}, {0, 1}, {1, 0}, {0, -1}
    };
    private static int m, n; // m - 1 行 n - 1 列
    private static boolean[][] visited;

    public static boolean exist(char[][] board, String word) {
        m = board.length;
        n = board[0].length;
        visited = new boolean[m][n];

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (searchWord(board, word, 0, i, j)) return true;
            }
        }

        return false;
    }

    /**
     * 从 board[startx][starty] 开始, 寻找 word[index ... word.length)
     */
    private static boolean searchWord(char[][] board, String word, int index, int startx, int starty) {
        if (index == word.length() - 1) return board[startx][starty] == word.charAt(index);

        if (board[startx][starty] == word.charAt(index)) {
            visited[startx][starty] = true;  // 做选择
            // 从 (startx, starty) 出发, 向四个方向寻找
            for (int i = 0; i < 4; i++) {
                int newx = startx + dir[i][0];
                int newy = starty + dir[i][1];
                if (inArea(newx, newy) && !visited[newx][newy] && searchWord(board, word, index + 1, newx, newy))
                    return true;
            }
            visited[startx][starty] = false; // 取消选择
        }

        return false;
    }

    private static boolean inArea(int x, int y) {
        return x >= 0 && x < m && y >= 0 && y < n;
    }
}
posted @ 2023-05-18 11:55  lidongdongdong~  阅读(31)  评论(0编辑  收藏  举报