回溯算法

151 阅读7分钟

image.png

1. leetcode112 路径总和

1.1 解法一:穷举所有路径

image.png 1. 找到所有根节点到叶子节点的路径
2. 判断是否存在路径和

1.2 解法二:计算每个节点的路径和

image.png 1. 相比上一个算法只需要记录每个节点的路径和即可无需记录所有路径,可以节省空间
2. res中如果有等于target的值则返回true,否则返回false

1.3 解法三:计算每个节点的目标和

image.png 1. res中如果有等于0的值则返回true,否则返回false

1.4 解法四:💖边遍历边判断

image.png

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null)
            return false;
        if (root.left == null && root.right == null) {
            return targetSum - root.val == 0;
        }
        boolean left = hasPathSum(root.left, (targetSum - root.val));
        if (left)
            return true;
        boolean right = hasPathSum(root.right, (targetSum - root.val));
        return left || right;
    }
}

2. leetcode113 路径总和II

2.1 解法一:💖计算每个节点的目标和

计算每个节点的目标和,当遍历到叶子节点时并且当前节点的目标和为0时,将此时的路径加入到结果集res

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<Integer> path = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        dfs(root, targetSum, path, res);
        return res;
    }

    private void dfs(TreeNode node,
                     int parentNodeTarget, // 父节点的目标和
                     List<Integer> path,
                     List<List<Integer>> res) {
        if (node == null)
            return;
        path.add(node.val);
        // 计算当前节点的目标和
        int currentNodeTarget = parentNodeTarget - node.val;
        // 当遍历到叶子节点时并且当前节点的目标和为0时
        if (node.left == null && node.right == null && currentNodeTarget == 0) {
            res.add(new ArrayList<>(path));
        }
        dfs(node.left, currentNodeTarget, path, res);
        dfs(node.right, currentNodeTarget, path, res);
        path.remove(path.size() - 1);
    }
}

3. leetcode46 全排列

3.1 回溯代码框架

1. 首先将其抽象成树形结构
2. 然后拿到数组所有的组合 image.png

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(nums, path, res);
        return res;
    }

    private void dfs(int[] nums,
                     List<Integer> path,
                     List<List<Integer>> res) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            path.add(nums[i]);
            dfs(nums, path, res);
            // 回溯的过程中,将当前的节点从 path 中删除
            path.remove(path.size() - 1);
        }
    }
}

3.2 解法一:💖剪枝去重(时间复杂度为O(n! * n²))

image.png

class Solution {
    // O(n! * n^2)
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        dfs(nums, path, res);
        return res;
    }

    private void dfs(int[] nums,
                     List<Integer> path,
                     List<List<Integer>> res) { // O(n^2)
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 剪枝,判断重复使用的数字
            if (path.contains(nums[i])) continue;
            path.add(nums[i]);
            dfs(nums, path, res);
            // 回溯的过程中,将当前的节点从 path 中删除
            path.remove(path.size() - 1);
        }
    }
}

3.3 解法二:💖剪枝去重替换成数组降低时间复杂度

class Solution {
    // O(n! * n)
    public List<List<Integer>> permute(int[] nums) {
        List<Integer> path = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        backtrack(nums, path, res, used);
        return res;
    }

    private void backtrack(int[] nums,
                           List<Integer> path,
                           List<List<Integer>> res,
                           boolean[] used) { // O(n)
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (used[i])
                continue;
            path.add(nums[i]);
            used[i] = true;
            backtrack(nums, path, res, used);
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }
}

4. leetcode47 全排列II

4.2 解法一:💖排序、去重

image.png 1. 去重的基础必须要先进行排序
2. 去重的条件:i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false

  • for循环保证了从数组中从前往后一个一个取值,再用if判断条件。所以nums[i - 1]一定比nums[i]先被取值和判断。如果nums[i - 1]被取值了,那used[i - 1]会被置true,只有当递归再回溯到这一层时再将它置false。每递归一层都是在寻找数组对应于递归深度位置的值,每一层里用for循环来寻找。所以当used[i - 1] == true时,说明nums[i - 1]nums[i]分别属于两层递归中,也就是我们要用这两个数分别放在数组的两个位置,这时不需要去重。但是当vis[i - 1] == false时,说明nums[i - 1]nums[i]属于同一层递归中,也就是我们要用这两个数放在数组中的同一个位置上,这就是我们需要去重的情况。见下图: image.png
class Solution {
    // O(n! * n)
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        boolean[] used = new boolean[nums.length];
        // 排序,去重的基础
        Arrays.sort(nums);
        dfs(nums, path, res, used);
        return res;
    }

    private void dfs(int[] nums,
                     List<Integer> path,
                     List<List<Integer>> res,
                     boolean[] used) { // O(n)
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 剪枝,判断重复使用的数字
            if (used[i] == true) 
                continue;
            // 去重的条件
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) 
                continue;
            path.add(nums[i]);
            used[i] = true;
            dfs(nums, path, res, used);
            // 回溯的过程中,将当前的节点从 path 中删除
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }
}

5. leetcode77 组合

5.1 解法一:💖先穷举,后剪枝

image.png image.png 1. 首先穷举所有的路径组合
2. 之后再进行剪枝

  • 每个数字只能使用一次
  • 结果不能重复——就是让子节点的值大于父节点的值
class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<Integer> combination = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        if (n <= 0 || k <= 0 || k > n)
            return res;
        backtrack(n, k, 1, combination, res);
        return res;
    }

    private void backtrack(int n,
                           int k,
                           int start,
                           List<Integer> combination,
                           List<List<Integer>> res) {
        if (combination.size() == k) {
            res.add(new ArrayList<>(combination));
            return;
        }
        for (int i = start; i <= n; i++) {
            combination.add(i);
            // 通过i + 1可以使每个数字只使用一次,并且结果不重复
            backtrack(n, k, i + 1, combination, res);
            combination.remove(combination.size() - 1);
        }
    }
}

6. leetcode39 组合总和

6.1 解法一:💖先穷举,后剪枝

image.png image.png 1. 先抽象成树形结构,穷举出所有的结果
2. 再进行剪枝

  • 结果不能重复
  • 路径和等于target
class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<Integer> combination = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        if (candidates == null || candidates.length == 0)
            return res;
        backtrack(candidates, 0, target, combination, res);
        return res;
    }

    private void backtrack(int[] candidates,
                           int startIndex,
                           int target,
                           List<Integer> combination,
                           List<List<Integer>> res) {
        // 通过两个if条件剪枝得到路径和等于target
        if (target < 0)
            return;
        if (target == 0) {
            res.add(new ArrayList<>(combination));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            combination.add(candidates[i]);
            // 引入startIndex,使得结果不能重复,让子节点的值大于等于父节点的值
            backtrack(candidates, i, target - candidates[i], combination, res);
            combination.remove(combination.size() - 1);
        }
    }
}

7. leetcode40 组合总和II

7.1 解法一:💖

1. 首先,需要对candidates进行排序,因为这是去重的基础
2. 解集不能包含重复的组合
3. candidates 中的每个数字在每个组合中只能使用一次

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<Integer> combination = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        if (candidates == null || candidates.length == 0)
            return res;
        // 1. 排序,这是去重的基础
        Arrays.sort(candidates);
        backtrack(candidates, 0, target, combination, res);
        return res;
    }

    private void backtrack(int[] candidates,
                           int startIndex,
                           int target,
                           List<Integer> combination,
                           List<List<Integer>> res) {
        if (target < 0)
            return;
        if (target == 0) {
            res.add(new ArrayList<>(combination));
            return;
        }
        for (int i = startIndex; i < candidates.length; i++) {
            // 为了保证数组元素访问的顺序,所有 i > startIndex
            // 2. 解决解集不能包含重复的组合
            if (i > startIndex && candidates[i] == candidates[i - 1])
                continue;
            combination.add(candidates[i]);
            // 3. candidates中的每个数字在每个组合中只能使用一次
            backtrack(candidates, i + 1, target - candidates[i], combination, res);
            combination.remove(combination.size() - 1);
        }
    }
}

8. leetcode78 子集

8.1 解法一:💖

1. 首先将其抽象成树形结构
2. 然后对其进行剪枝操作 image.png image.png image.png

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        List<Integer> subset = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0)
            return res;
        backtrack(nums, 0, subset, res);
        return res;
    }

    private void backtrack(int[] nums,
                           int startIndex,
                           List<Integer> subset,
                           List<List<Integer>> res) {
        // 无限制条件,每次遍历到一个节点都将其加入到结果集中
        res.add(new ArrayList<>(subset));
        for (int i = startIndex; i < nums.length; i++) {
            subset.add(nums[i]);
            backtrack(nums, i + 1, subset, res);
            subset.remove(subset.size() - 1);
        }
    }
}

9. leetcode90 子集II

9.1 解法一:💖

1. 数组中包含重复元素,因此需要排序
2. 解集中不能包含重复的组合

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<Integer> subset = new ArrayList<>();
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length == 0)
            return res;
        // 1. 数组中包含重复元素,因此需要排序
        Arrays.sort(nums);
        backtrack(nums, 0, subset, res);
        return res;
    }

    private void backtrack(int[] nums,
                           int startIndex,
                           List<Integer> subset,
                           List<List<Integer>> res) {
        res.add(new ArrayList<>(subset));
        for (int i = startIndex; i < nums.length; i++) {
            // 2. 解集中不能包含重复的组合
            if (i > startIndex && nums[i] == nums[i - 1])
                continue;
            subset.add(nums[i]);
            backtrack(nums, i + 1, subset, res);
            subset.remove(subset.size() - 1);
        }
    }
}

10. leetcode17 电话号码的字母组合

10.1 解法一:💖使用HashMap存储映射

image.png

class Solution {
    private Map<Character, String> phone = 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");
    }};

    public List<String> letterCombinations(String digits) {
        List<String> res = new ArrayList<>();
        if (digits == null || digits.length() == 0)
            return res;
        backtrack(digits, 0, new StringBuilder(), res);
        return res;
    }

    private void backtrack(String digits,
                           int index,
                           StringBuilder sb,
                           List<String> res) {
        if (index == digits.length()) {
            res.add(sb.toString());
            return;
        }
        char numChar = digits.charAt(index);
        char[] letters = phone.get(numChar).toCharArray();
        for (char c : letters) {
            sb.append(c);
            backtrack(digits, index + 1, sb, res);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

10.2 解法二:💖使用数组字符串数组存储映射

class Solution {
    private String[] phone = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

    public List<String> letterCombinations(String digits) {
        List<String> res = new ArrayList<>();
        if (digits == null || digits.length() == 0)
            return res;
        backtrack(digits, 0, new StringBuilder(), res);
        return res;
    }

    private void backtrack(String digits,
                           int index,
                           StringBuilder sb,
                           List<String> res) {
        if (index == digits.length()) {
            res.add(sb.toString());
            return;
        }
        char numChar = digits.charAt(index);
        char[] letters = phone[digits.charAt(index) - '2'].toCharArray();
        for (char c : letters) {
            sb.append(c);
            backtrack(digits, index + 1, sb, res);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

11. leetcode93 复原IP地址

11.1 解法一:💖

1. 判断ip段是否合法

  • ip段的长度不能大于3
  • ip段如果是以0开始的话,那么这个ip段只能是0
  • ip段需要小于等于255 2. 抽象成树形结构 image.png 3. 代码实现:
class Solution {
    private List<String> res;

    public List<String> restoreIpAddresses(String s) {
        res = new ArrayList<>();
        if (s == null || s.length() == 0)
            return res;
        backtrack(s, 0, "", 0);
        return res;
    }

    private void backtrack(String s,
                           int index, // 记录遍历s的索引值
                           String restored, // 存储ip段
                           int count) { // 记录ip的段数
        if (count > 4)
            return;
        if (count == 4 && index == s.length()) {
            res.add(restored);
            return;
        }
        for (int segmentLen = 1; segmentLen < 4; segmentLen++) {
            // 取字符串的边界条件,不可超过原有字符串的长度
            if (index + segmentLen > s.length())
                break;
            // substring是左闭右开
            String segment = s.substring(index, index + segmentLen);
            // 检查ip段是否属于合法字段
            if (!isValidIpSegment(segment))
                continue;
            // ip段需要加三个点0、1、2,当count==3时不再添加
            String suffix = count == 3 ? "" : ".";
            backtrack(s, index + segmentLen, restored + segment + suffix, count + 1);
        }
    }

    private boolean isValidIpSegment(String segment) {
        // 1. 长度不能大于3
        int len = segment.length();
        if (len > 3)
            return false;
        // 1. ip段如果是以0开始的话,那么这个ip段只能是0
        // 2. ip段需要小于等于255
        return (segment.charAt(0) == '0') ?
                (len == 1) : (Integer.valueOf(segment) <= 255);
    }
}

12. leetcode22 括号生成

12.1 解法一:💖

1. 抽象成树形问题
2. 然后进行剪枝

  • 左括号的数量不能大于n
  • 右括号的数量不能大于左括号的数量 image.png image.png 3. 代码实现:
class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        if (n <= 0)
            return res;
        backtrack(n, "", res, 0, 0);
        return res;
    }

    private void backtrack(int n,
                           String path,
                           List<String> res,
                           int open,
                           int close) {
        if (open > n || close > open)
            return;
        if (path.length() == n * 2) {
            res.add(path);
            return;
        }
        backtrack(n, path + "(", res, open + 1, close);
        backtrack(n, path + ")", res, open, close + 1);
    }
}

12.2 解法二:💖

1. 代码实现:

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

    private void backtrack(int n,
                           String path,
                           List<String> res,
                           int open,
                           int close) {
        if (path.length() == n * 2) {
            res.add(path);
            return;
        }
        if (open < n)
            backtrack(n, path + "(", res, open + 1, close);
        if (close < open)
            backtrack(n, path + ")", res, open, close + 1);
    }
}

13. leetcode51 N皇后

13.1 解法一:💖

1. 穷举皇后位置 image.png 2. 抽象成树形结构 image.png 3. 判断是否攻击逻辑

  • 利用主对角线row - col + n - 1唯一确定主对角线方向
  • 利用副对角线row + col唯一确定副对角线方向 4. 代码实现:
class Solution {
    private int n;
    // 存储皇后的位置
    private int[] rows;
    // 标记是否被列方向的皇后攻击
    private int[] cols;
    // 标记是否被主对角线方向的皇后攻击
    private int[] mains;
    // 标记是否被副对角线方向的皇后攻击
    private int[] secondary;

    private List<List<String>> res;

    public List<List<String>> solveNQueens(int n) {
        this.n = n;
        this.rows = new int[n];
        this.cols = new int[n];
        this.mains = new int[2 * n - 1];
        this.secondary = new int[2 * n - 1];
        this.res = new ArrayList<>();
        backtrack(0);
        return res;
    }

    private void backtrack(int row) {
        if (row >= n)
            return;
        // 分别尝试在当前行的每一列中放置皇后
        for (int col = 0; col < n; col++) {
            if (isNotUnderAttack(row, col)) {
                // 选择在当前位置上放置皇后
                placeQueen(row, col);
                // 当前行是最后一行,则找到了一个解决方案
                if (row == n - 1)
                    addSolution();
                // 在下一行放置皇后
                backtrack(row + 1);
                // 回溯,即将当前位置的皇后去掉
                removeQueen(row, col);
            }
        }
    }

    // 在指定位置上放置皇后
    private void placeQueen(int row, int col) {
        // 在row行,col列放置皇后
        rows[row] = col;
        // 当前位置的列方向已经有皇后了
        cols[col] = 1;
        // 当前位置的主对角线方向已经有皇后了
        mains[row - col + n - 1] = 1;
        // 当前位置的副对角线已经有皇后了
        secondary[row + col] = 1;
    }

    private void removeQueen(int row, int col) {
        // 移除row行上的皇后
        rows[row] = 0;
        // 当前位置的列方向没有皇后了
        cols[col] = 0;
        // 当前位置的主对角线方向没有皇后了
        mains[row - col + n - 1] = 0;
        // 当前位置的副对角线方向没有皇后了
        secondary[row + col] = 0;
    }

    private void addSolution() {
        List<String> solution = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            int col = rows[i];
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < col; j++) {
                sb.append(".");
            }
            sb.append("Q");
            for (int j = 0; j < n - col - 1; j++) {
                sb.append(".");
            }
            solution.add(sb.toString());
        }
        res.add(solution);
    }

    private boolean isNotUnderAttack(int row, int col) {
        // 判断的逻辑是:
        // 1. 当前位置的这一列方向没有皇后攻击
        // 2. 当前位置的主对角线方向没有皇后攻击
        // 3. 当前位置的副对角线方向没有皇后攻击
        int result = cols[col] + mains[row - col + n - 1] + secondary[row + col];
        return result == 0;
    }
}

14. leetcode37 解数独

14.1 解法一:🙌

image.png

class Solution {
    public void solveSudoku(char[][] board) {
        // 用于标识数字是否在行、列、箱子中使用过
        boolean[][] rowUsed = new boolean[9][10];
        boolean[][] colUsed = new boolean[9][10];
        boolean[][][] boxUsed = new boolean[3][3][10];
        // 初始化
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board.length; j++) {
                int num = board[i][j] - '0';
                if (num >= 1 && num <= 9) {
                    rowUsed[i][num] = true;
                    colUsed[j][num] = true;
                    boxUsed[i / 3][j / 3][num] = true;
                }
            }
        }
        backtrack(board, 0, 0, rowUsed, colUsed, boxUsed);
    }

    private boolean backtrack(char[][] board,
                              int row,
                              int col,
                              boolean[][] rowUsed,
                              boolean[][] colUsed,
                              boolean[][][] boxUsed) {
        if (col == board[0].length) {
            // 下一行
            row++;
            // 第一列
            col = 0;
            if (row == board.length) {
                return true;
            }
        }

        if (board[row][col] == '.') {
            // 尝试填充1到9数字
            for (int num = 1; num <= 9; num++) {
                boolean canPlaced = rowUsed[row][num] == false &&
                        colUsed[col][num] == false &&
                        boxUsed[row / 3][col / 3][num] == false;
                if (canPlaced) {
                    rowUsed[row][num] = true;
                    colUsed[col][num] = true;
                    boxUsed[row / 3][col / 3][num] = true;
                    board[row][col] = (char) ('0' + num);

                    // 尝试填充下一个空格
                    if (backtrack(board, row, col + 1, rowUsed, colUsed, boxUsed)) {
                        return true;
                    }

                    // 否则的话回溯
                    board[row][col] = '.';
                    rowUsed[row][num] = false;
                    colUsed[col][num] = false;
                    boxUsed[row / 3][col / 3][num] = false;
                }
            }
        } else {
            return backtrack(board, row, col + 1, rowUsed, colUsed, boxUsed);
        }
        return false;
    }
}

15. leetcode401 二进制手表

15.1 解法一:组合类回溯✨

1. 将其抽象成组合问题进行解决 image.png

class Solution {
    public List<String> readBinaryWatch(int turnedOn) {
        int[] nums1 = {8, 4, 2, 1};
        int[] nums2 = {32, 16, 8, 4, 2, 1};

        List<String> res = new ArrayList<>();
        for (int i = 0; i <= turnedOn; i++) {
            // 拿到i个小时的组合
            List<Integer> hours = findCombinations(nums1, i);
            // 拿到turnedOn - i个分钟的组合
            List<Integer> minutes = findCombinations(nums2, turnedOn - i);
            for (int hour : hours) {
                if (hour > 11)
                    continue;
                for (int minute : minutes) {
                    if (minute > 59)
                        continue;
                    String minuteStr = (minute < 10) ? "0" + minute : minute + "";
                    res.add(hour + ":" + minuteStr);
                }
            }
        }
        return res;
    }

    private List<Integer> findCombinations(int[] nums, int count) {
        List<Integer> res = new ArrayList<>();
        backtrack(nums, count, 0, 0, res);
        return res;
    }

    private void backtrack(int[] nums, int count, int sum, int start, List<Integer> res) {
        if (count == 0) {
            res.add(sum);
            return;
        }
        for (int i = start; i < nums.length; i++) {
            backtrack(nums, count - 1, sum + nums[i], i + 1, res);
        }
    }
}

16. leetcode131 分割回文串

16.1 解法一:子集类回溯✨

1. 抽象成树形结构
image.png

class Solution {
    private String s;
    private List<List<String>> res;

    public List<List<String>> partition(String s) {
        this.s = s;
        this.res = new ArrayList<>();
        backtrack(0, new ArrayList<>());
        return res;
    }

    private void backtrack(int startIndex, List<String> path) {
        if (startIndex == s.length()) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            int endIndex = i;
            // [startIndex, endIndex]
            // 如果不是回文串则进行剪枝
            if (!checkPalindrome(s, startIndex, endIndex))
                continue;
            path.add(s.substring(startIndex, endIndex + 1));
            backtrack(i + 1, path);
            path.remove(path.size() - 1);
        }
    }

    private boolean checkPalindrome(String str, int left, int right) {
        while (left < right) {
            if (s.charAt(left) != s.charAt(right))
                return false;
            left++;
            right--;
        }
        return true;
    }
}

17. leetcode842 将数组拆分成斐波那契序列

17.1 解法一:子集类回溯✨

1. 抽象成树形结构
image.png image.png

class Solution {
    private String num;
    private List<Integer> res;

    public List<Integer> splitIntoFibonacci(String num) {
        this.num = num;
        this.res = new ArrayList<>();
        backtrack(0, 0, 0);
        return res;
    }

    private boolean backtrack(int startIndex, int prevTwoNumSum, int prevNum) {
        if (startIndex == num.length()) {
            return res.size() >= 3;
        }

        long currLongNum = 0;
        for (int i = startIndex; i < num.length(); i++) {
            // 如果当前字符是'0'的话,则不作处理,因为数字不能以0开头
            if (i > startIndex && num.charAt(startIndex) == '0')
                continue;
            currLongNum = currLongNum * 10 + (num.charAt(i) - '0');
            if (currLongNum > Integer.MAX_VALUE)
                break;
            int currNum = (int) currLongNum;
            if (res.size() >= 2) {
                if (currNum < prevTwoNumSum)
                    continue;
                else if (currNum > prevTwoNumSum)
                    break;
            }
            res.add(currNum);
            if (backtrack(i + 1, currNum + prevNum, currNum))
                return true;
            res.remove(res.size() - 1);
        }
        return false;
    }
}

18. leetcode79 单词搜索

18.1 解法一:✨flood fill + 回溯

class Solution {
    private char[][] board;
    private String word;
    private int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    private int m;
    private int n;
    private boolean[][] visited;

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

        for (int row = 0; row < m; row++) {
            for (int col = 0; col < n; col++) {
                if (board[row][col] == word.charAt(0)) {
                    if (dfs(row, col, 0))
                        return true;
                }
            }
        }
        return false;
    }

    private boolean dfs(int row, int col, int index) {
        if (board[row][col] != word.charAt(index))
            return false;
        else if (index == word.length() - 1)
            return true;
        visited[row][col] = true;
        for (int[] dir : dirs) {
            int nextRow = row + dir[0];
            int nextCol = col + dir[1];
            if (nextRow >= 0 && nextRow < m &&
                    nextCol >= 0 && nextCol < n &&
                    !visited[nextRow][nextCol]) {
                if (dfs(nextRow, nextCol, index + 1))
                    return true;
            }
        }
        visited[row][col] = false;
        return false;
    }
}
class Solution {
    public boolean exist(char[][] board, String word) {
        if(board == null || board.length == 0)
            return false;
        char[] words = word.toCharArray();
        for(int row = 0; row < board.length; row++){
            for(int col = 0; col < board[0].length; col++){
                if(dfs(board, words, row, col, 0)){
                    return true;
                }
            }
        }
        return false;
    }

    private boolean dfs(char[][] board, char[] words, int row, int col, int index){
        if(row < 0 || row >= board.length || col < 0 || col >= board[0].length || board[row][col] != words[index])
            return false;
        if(index == words.length - 1)
            return true;
        char temp = board[row][col];
        board[row][col] = '.';
        boolean res = dfs(board, words, row - 1, col, index + 1) || //上
                      dfs(board, words, row, col + 1, index + 1) || //右
                      dfs(board, words, row + 1, col, index + 1) || //下
                      dfs(board, words, row, col - 1, index + 1); //左
        board[row][col] = temp;
        return res;
    }
}

19. leetcode301 删除无效的括号

解法一:BFS✨

image.png

class Solution {
    public List<String> removeInvalidParentheses(String s) {
        List<String> res = new ArrayList<>();
        Queue<String> queue = new LinkedList<>();
        queue.add(s);
        Set<String> visited = new HashSet<>();
        visited.add(s);
        boolean found = false;
        while (!queue.isEmpty()) {
            // 最优解一定在同一层
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currStr = queue.poll();
                if (isValid(currStr)) {
                    res.add(currStr);
                    found = true;
                    continue;
                }
                int currStrLen = currStr.length();
                for (int j = 0; j < currStrLen; j++) {
                    if (currStr.charAt(j) != '(' && currStr.charAt(j) != ')')
                        continue;
                    String leftStr = currStr.substring(0, j);
                    String rightStr = (j == currStrLen - 1) ?
                            "" : currStr.substring(j + 1, currStrLen);
                    String next = leftStr + rightStr;
                    if (!visited.contains(next)) {
                        queue.add(next);
                        visited.add(next);
                    }
                }
            }
            if (found)
                break;
        }
        return res;
    }

    private boolean isValid(String s) {
        int count = 0;
        for (char c : s.toCharArray()) {
            if (c == '(')
                count++;
            else if (c == ')')
                count--;
            if (count < 0)
                return false;
        }
        return count == 0;
    }
}

解法二:DFS✨

1. 首先遍历字符串得到最少应该删除的左括号个数leftRemove与右括号个数rightRemove
2. 之后抽象成树形结构,使用回溯算法求得结果 image.png image.png image.png

class Solution {
    private String s;
    private Set<String> res;

    public List<String> removeInvalidParentheses(String s) {
        this.s = s;
        this.res = new HashSet<>();
        // 第 1 步:遍历一次,计算多余的左右括号
        int leftRemove = 0;
        int rightRemove = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                leftRemove++;
            } else if (s.charAt(i) == ')') {
                // 遇到右括号的时候,需要根据已经存在的左括号数量决定
                if (leftRemove == 0) {
                    rightRemove++;
                }
                if (leftRemove > 0) {
                    // 关键:一个右括号出现可以抵消之前遇到的左括号
                    leftRemove--;
                }
            }
        }

        // 第2步:回溯算法,尝试每一种可能的删除操作
        backtrack(0, leftRemove, rightRemove, 0, 0, new StringBuilder());
        return new ArrayList<>(res);
    }

    private void backtrack(int index,
                           int leftRemove, int rightRemove,
                           int leftCount, int rightCount,
                           StringBuilder path) {
        if (index == s.length()) {
            if (leftRemove == 0 && rightRemove == 0) {
                res.add(path.toString());
            }
            return;
        }
        // 先处理当前的字符
        char c = s.charAt(index);
        // 可能的操作 1:删除当前遍历到的字符
        // 删除左括号
        if (c == '(' && leftRemove > 0) {
            backtrack(index + 1, leftRemove - 1, rightRemove, leftCount, rightCount, path);
        }
        // 删除右括号
        if (c == ')' && rightRemove > 0) {
            backtrack(index + 1, leftRemove, rightRemove - 1, leftCount, rightCount, path);
        }
        // 可能的操作 2:保留当前遍历道德字符
        path.append(c);
        if (c != '(' && c != ')') {
            // 如果是其他字符
            backtrack(index + 1, leftRemove, rightRemove, leftCount, rightCount, path);
        } else if (c == '(') {
            // 如果是左括号
            backtrack(index + 1, leftRemove, rightRemove, leftCount + 1, rightCount, path);
        } else if (rightCount < leftCount) {
            // 如果是右括号并且右括号数量小于左括号数量
            backtrack(index + 1, leftRemove, rightRemove, leftCount, rightCount + 1, path);
        }
        path.deleteCharAt(path.length() - 1);
    }
}

20. leetcode679 24点游戏

解法一:回溯算法✨

1. 将其抽象成树形结构再dfs
image.png

class Solution {
    static final int TARGET = 24;
    static final double EPSILON = 1e-6;
    static final int ADD = 0, MULTIPLY = 1, SUBTRACT = 2, DIVIDE = 3;

    public boolean judgePoint24(int[] cards) {
        List<Double> list = new ArrayList<>();
        for (int card : cards) {
            list.add((double) card);
        }
        return backtrack(list);
    }

    private boolean backtrack(List<Double> list) {
        if (list.size() == 0)
            return false;
        if (list.size() == 1)
            return Math.abs(list.get(0) - TARGET) < EPSILON;
        int size = list.size();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (i != j) {
                    List<Double> childList = new ArrayList<>();
                    for (int k = 0; k < size; k++) {
                        if (k != i && k != j) {
                            childList.add(list.get(k));
                        }
                    }
                    for (int k = 0; k < 4; k++) {
                        // 0 + 1 = 1 + 0
                        // 1 * 2 = 2 * 1 需要剪枝
                        if (k < 2 && i > j)
                            continue;
                        if (k == ADD) {
                            childList.add(list.get(i) + list.get(j));
                        } else if (k == MULTIPLY) {
                            childList.add(list.get(i) * list.get(j));
                        } else if (k == SUBTRACT) {
                            childList.add(list.get(i) - list.get(j));
                        } else if (k == DIVIDE) {
                            if (Math.abs(list.get(j)) < EPSILON) {
                                continue;
                            } else {
                                childList.add(list.get(i) / list.get(j));
                            }
                        }
                        if (backtrack(childList))
                            return true;
                        childList.remove(childList.size() - 1);
                    }
                }
            }
        }
        return false;
    }
}