代码随想录刷题-回溯

1.回溯模版

List result = new ArrayList();
public void backtrack(路径, 选择列表){
    if(满足结束条件){
		result.add(路径);
        return;
	}
    
    for(选择 : 选择列表){
        做选择;
        backtrack(路径, 选择列表); //递归遍历
        撤销选择;
    }
}
  • 子集:所有节点

    • istart 开始,因为不可以往前选
  • 组合:某一层的节点

    • istart 开始,因为不可以往前选
    • 也可以看成某一层的子集,所以相比子集 res 添加 path 时要加 if 判断
  • 排列:叶子节点

    • i0 开始,因为可以往前选

2.三种形式总结

精简总结

  • 无重可复选最简单,什么也不用管
  • 可重要剪枝,排序后和前一个相同要跳过,排列前一个还必须要使用过
  • 不可复选组合/子集下次递归从 i+1 开始,排列加 used 数组判断是否选过

详细总结:

  • 可重/不可重
    • 元素可重那就会有重复,那就要剪枝
    • 对于子集/组合,要加排序和 nums[i] == nums[i - 1] 判断
    • 排列的可重还要加上 !used[i - 1] 才能剪枝,这样才能保证元素的相对位置不变
  • 可复选/不可复选
    • 不可复选意味着必须选没用过的,那怎么判断用没用过呢?
    • 对于子集/组合
      • 可复选下次递归就从当前开始,那 backtarck 函数的参数是 i 就行,这样下次还能选当前元素
      • 不可复选那每次必须选后面的元素,所以 backtarck 函数的参数是 i+1
    • 排列:排列递归参数没有 i,要加 used 数组判断是否选过

形式一 :元素无重可复选

元素无重可复选,即 nums 中的元素都是唯一的,每个元素可以被使用若干次,只要删掉去重逻辑即可,backtrack 核心代码如下:

/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // i 从 start 开始
    for (int i = start; i < nums.length; i++) {
        path.addLast(nums[i]);
        // 注意参数是 i
        backtrack(nums, i);
        path.removeLast();
    }
}


/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    // i 从 0 开始
    for (int i = 0; i < nums.length; i++) {
        path.add(nums[i]);
        backtrack(nums);
        path.removeLast();
    }
}

形式二 :元素无重不可复选

元素无重不可复选,即 nums 中的元素都是唯一的,每个元素最多只能被使用一次backtrack 核心代码如下:

/* 组合/子集问题回溯算法框架 */
void backtrack(int[] nums, int start) {
    // i 从 start 开始
    for (int i = start; i < nums.length; i++) {
        path.add(nums[i]);
        // 注意参数是 i+1
        backtrack(nums, i + 1);
        path.removeLast();
    }
}

/* 排列问题回溯算法框架 */
void backtrack(int[] nums) {
    // i 从 0 开始
    for (int i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        used[i] = true;
        track.addLast(nums[i]);
        backtrack(nums);
        track.removeLast();
        used[i] = false;
    }
}

形式三 :元素可重不可复选

元素可重不可复选,即 nums 中的元素可以存在重复,每个元素最多只能被使用一次,其关键在于排序和剪枝,backtrack 核心代码如下:

/* 组合/子集问题回溯算法框架 */
// 排序加剪枝
Arrays.sort(nums);
void backtrack(int[] nums, int start) {
    // i 从 start 开始
    for (int i = start; i < nums.length; i++) {
        // 剪枝逻辑,跳过值相同的相邻树枝
        if (i > start && nums[i] == nums[i - 1]) {
            continue;
        }
        track.addLast(nums[i]);
        // 注意参数是 i+1
        backtrack(nums, i + 1);
        track.removeLast();
    }
}

/* 排列问题回溯算法框架 */
Arrays.sort(nums);
void backtrack(int[] nums) {
    // i 从 0 开始
    for (int i = 0; i < nums.length; i++) {
        // 剪枝逻辑
        if (used[i]) {
            continue;
        }
        // 剪枝逻辑,固定相同的元素在排列中的相对位置
        if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
            continue;
        }
        used[i] = true;
        track.addLast(nums[i]);
        backtrack(nums);
        track.removeLast();
        used[i] = false;
    }
}

3.组合

3-77.组合🟡

题目:代码:给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

链接:77. 组合

代码:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        backtrack(n, k, 1);
        return result;
    }

    // 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
    private void backtrack(int n, int k, int startIndex) {
        //终止条件
        if (path.size() == k) {
            // new一个对象从而不影响后续path的值
            result.add(new ArrayList<>(path));
            return;
        }
        // for (int i =startIndex;i<=n;i++){的剪枝版本
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            path.add(i);
            backtrack(n, k, i + 1);
            path.removeLast();
        }
    }
}

4-216.组合总和 III🟡

题目:找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

链接:216. 组合总和 III

代码:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        backtrack(n, k, 1, 0);
        return result;
    }

    private void backtrack(int targetSum, int k, int startIndex, int sum) {
        // 减枝
        if (sum > targetSum) {
            return;
        }

        if (path.size() == k) {
            if (sum == targetSum)
                result.add(new ArrayList<>(path));
            return;
        }

        // 减枝 9 - (k - path.size()) + 1
        for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
            path.add(i);
            sum += i;
            backtrack(targetSum, k, i + 1, sum);
            // 回溯
            path.removeLast();
            sum -= i;
        }
    }
}

5-17.电话号码的字母组合🟡

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

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

链接:17. 电话号码的字母组合

代码:

class Solution {
    // 每个数字到字母的映射
    String[] mapping = new String[]{
            "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
    };
    List<String> res = new LinkedList<>();

    public List<String> letterCombinations(String digits) {
        if (digits.isEmpty()) {
            return res;
        }
        // 从 digits[0] 开始进行回溯
        backtrack(digits, 0, new StringBuilder());
        return res;
    }

    // 回溯算法主函数
    void backtrack(String digits, int start, StringBuilder sb) {
        if (sb.length() == digits.length()) {
            // 到达回溯树底部
            res.add(sb.toString());
            return;
        }
        // 回溯算法框架
        for (int i = start; i < digits.length(); i++) {
            int digit = digits.charAt(i) - '0';
            for (char c : mapping[digit].toCharArray()) {
                // 做选择
                sb.append(c);
                // 递归下一层回溯树
                backtrack(digits, i + 1, sb);
                // 撤销选择
                sb.deleteCharAt(sb.length() - 1);
            }
        }
    }
}

7-39.组合总和🟡

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

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

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

链接:39. 组合总和

代码:

class Solution {
    public List<List<Integer>> res = new ArrayList<>();
    public LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, 0);
        return res;
    }

    public void backtrack(int[] candidates, int target, int sum, int index) {
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }
        // 在循环时进行剪枝,需要对数组进行排序
        for (int i = index; i < candidates.length && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.add(candidates[i]);
            backtrack(candidates, target, sum, i);
            sum -= candidates[i];
            path.removeLast();
        }
    }
}

8-40. 组合总和 II🟡

题目:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

注意: 解集不能包含重复的组合。

链接:40. 组合总和 II

代码:

class Solution {
    public List<List<Integer>> res = new ArrayList<>();
    public LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 为了将重复的数字都放到一起,所以先进行排序
        Arrays.sort(candidates);
        backtrack(candidates, target, 0, 0);
        return res;
    }

    public void backtrack(int[] candidates, int target, int sum, int index) {
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = index; i < candidates.length && sum + candidates[i] <= target; i++) {
            // 跳过同一树层使用过的元素
            if (i > index && candidates[i] == candidates[i - 1])
                continue;
            sum += candidates[i];
            path.add(candidates[i]);
            // 每个元素只能出现一次,所以是i+1
            backtrack(candidates, target, sum, i + 1);
            sum -= path.get(path.size() - 1);
            path.removeLast();
        }
    }
}

9-131.分割回文串🟡

题目:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

链接:131. 分割回文串

代码:

class Solution {
    List<List<String>> res = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();

    public List<List<String>> partition(String s) {
        backtrack(s, 0);
        return res;
    }

    private void backtrack(String s, int start) {
        if (start >= s.length()) {
            // 如果起始位置大于s的大小,说明找到了一组分割方案
            res.add(new ArrayList<String>(path));
            return;
        }
        for (int i = start; i < s.length(); i++) {
            if (!isPalindrome(s, start, i)) {
                // s[start..i] 不是回文串,不能分割
                continue;
            }
            // s[start..i] 是一个回文串,可以进行分割str
            // 做选择,把 s[start..i] 放入路径列表中
            path.addLast(s.substring(start, i + 1));
            // 进入回溯树的下一层,继续切分 s[i+1..]
            backtrack(s, i + 1);
            // 撤销选择
            path.removeLast();
        }
    }

    // 用双指针技巧判断 s[start..end] 是否是一个回文串
    private boolean isPalindrome(String s, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
        }
        return true;
    }
}

10-93.复原 IP 地址🟡

题目:有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201""192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

链接:93. 复原 IP 地址

代码:

class Solution {
    List<String> res = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();

    public List<String> restoreIpAddresses(String s) {
        backtrack(s, 0);
        return res;
    }

    void backtrack(String s, int start) {
        // 即整个 s 被成功分割为合法的四部分,记下答案
        if (start == s.length() && path.size() == 4) {
            // join函数直接用.拼接path
            res.add(String.join(".", path));
        }
        for (int i = start; i < s.length(); i++) {
            if (!isValid(s, start, i)) {
                continue;
            }
            path.addLast(s.substring(start, i + 1));
            backtrack(s, i + 1);
            path.removeLast();
        }
    }

    boolean isValid(String s, int start, int end) {
        // 位数大于3
        if (end - start + 1 > 3) {
            return false;
        }
        if (s.charAt(start) == '0' && start != end) {
            return false;
        }
        int i = Integer.parseInt(s.substring(start, end + 1));
        return i <= 255;
    }
}

4.子集

11-78.子集🟡

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

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

链接:78. 子集

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }

    public void backtrack(int[] nums, int start) {
        // 不是只记录叶子节点,而是记录所有节点,所以不用if判断
        res.add(new LinkedList<>(path));
        // 终止条件可不加
        if (start >= nums.length) {
            return;
        }
        for (int i = start; i < nums.length; i++) {
            path.add(nums[i]);
            backtrack(nums, i + 1);
            path.removeLast();
        }
    }
}

13-90.子集 II🟡

题目:给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。

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

链接:90. 子集 II

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        // 先排序,让相同的元素靠在一起
        Arrays.sort(nums);
        backtrack(nums, 0);
        return res;
    }
    private void backtrack(int[] nums, int start) {
        // 前序位置,每个节点的值都是一个子集
        res.add(new ArrayList<>(path));

        for (int i = start; i < nums.length; i++) {
            // 跳过当前树层使用过的、相同的元素
            if (i > start && nums[i - 1] == nums[i]) {
                continue;
            }
            path.add(nums[i]);
            backtrack(nums, i + 1);
            path.removeLast();
        }
    }
}

14-491.非递减子序列🟡

题目:给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

链接:491. 非递减子序列

代码:

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        backtrack(nums, 0);
        return res;
    }

    void backtrack(int[] nums, int start) {
        if (path.size() >= 2) {
            res.add(new LinkedList<>(path));
        }
        // 用哈希集合防止重复选择相同元素
        // 题目说明-100 <= nums[i] <= 100,所以也可以用一个used数组去重
        // int[] used = new int[201];
        HashSet<Integer> used = new HashSet<>();

        for (int i = start; i < nums.length; i++) {
            // 保证集合中元素都是递增顺序
            if (!path.isEmpty() && path.getLast() > nums[i]) {
                continue;
            }
            // 保证不要重复使用相同的元素
            if (used.contains(nums[i])) {
                continue;
            }
            used.add(nums[i]);
            path.add(nums[i]);
            backtrack(nums, i + 1);
            path.removeLast();
        }
    }
}

5.排列

15-46.全排列🟡

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

链接:46. 全排列

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> permute(int[] nums) {
        boolean[] used = new boolean[nums.length];
        backtrack(nums, used);
        return res;
    }

    private void backtrack(int[] nums, boolean[] used) {
        // 到达叶子节点
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }
        // 注意从0开始,可以往回选
        // [1,2]中可以选择[1,2]也可以是[2,1]
        for (int i = 0; i < nums.length; i++) {
            // 因为从0开始,所以会重复选择元素,用used去重
            if (used[i])
                continue;
            path.add(nums[i]);
            used[i] = true;
            backtrack(nums, used);
            path.removeLast();
            used[i] = false;
        }
    }
}

16-47.全排列 II🟡

题目:给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

链接:47. 全排列 II

代码:

// 与上一题全排列的区别是本题有重复数据
class Solution {

    List<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> permuteUnique(int[] nums) {
        // 先排序,让相同的元素靠在一起
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        backtrack(nums, used);
        return res;
    }

    void backtrack(int[] nums, boolean[] used) {
        if (path.size() == nums.length) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            // 固定相同的元素在排列中的相对位置,前一个元素必须使用过
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            path.add(nums[i]);
            used[i] = true;
            backtrack(nums, used);
            path.removeLast();
            used[i] = false;
        }
    }
}

6.棋盘

20-51.N 皇后🔴

题目:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

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

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

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

链接:51. N 皇后

代码:

// 棋盘是个n×n的数组,因此可以将第i行看成树的第i层
// 第j列看成树的第j个分支
class Solution {
    List<List<String>> res = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        // 填充棋盘元素为.
        char[][] chessboard = new char[n][n];
        for (char[] cArr : chessboard) {
            Arrays.fill(cArr, '.');
        }
        backtrack(n, chessboard, 0);
        return res;
    }

    public void backtrack(int n, char[][] chessboard, int row) {
        if (row == n) {
            // 数组转列表
            List<String> sList = new ArrayList<>();
            for (char[] cArr : chessboard) {
                sList.add(String.valueOf(cArr));
            }
            res.add(sList);
        }
        for (int col = 0; col < n; col++) {
            if (isValid(n, chessboard, row, col)) {
                chessboard[row][col] = 'Q';
                backtrack(n, chessboard, row + 1);
                chessboard[row][col] = '.';
            }
        }
    }

    public boolean isValid(int n, char[][] chessboard, int row, int col) {
        // 检查列
        for (int i = 0; i < row; i++) {
            if (chessboard[i][col] == 'Q') {
                return false;
            }
        }
        // 检查左上角
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        // 检查右上角
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
}

21-37.解数独🔴

题目:编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

链接:37. 解数独

代码:

// 从根部一直往下找符合条件的叶子节点,不行就回溯
// 若找到一条符合条件的路径,立刻返回
class Solution {
    public void solveSudoku(char[][] board) {
        backtrack(board);
    }

    // 找到符合条件的值就立即返回,因此回溯需要有boolean返回值
    public boolean backtrack(char[][] board) {
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                if (board[i][j] != '.')
                    continue;
                for (char k = '1'; k <= '9'; k++) {
                    // 判断 board[i][j] 是否可以填入 k
                    if (isValid(i, j, k, board)) {
                        board[i][j] = k;
                        // 如果找到数独的一组解立刻返回
                        if (backtrack(board))
                            return true;
                        board[i][j] = '.';
                    }
                }
                // 当前的空白格不能填下任何一个数字,返回false进行回溯
                return false;
            }
        }
        // 遍历完棋盘也没返回false,说明找到了一组解
        return true;
    }

    // 判断 board[i][j] 是否可以填入 k
    public boolean isValid(int row, int col, char k, char[][] board) {
        for (int i = 0; i < 9; i++) {
            // 判断行是否存在重复
            if (board[row][i] == k)
                return false;
            // 判断列是否存在重复
            if (board[i][col] == k)
                return false;
        }
        // 判断 3 x 3 方框是否存在重复
        int boxRow = (row / 3) * 3; // 方框的起始行索引
        int boxCol = (col / 3) * 3; // 方框的起始列索引
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (board[boxRow + i][boxCol + j] == k) {
                    return false;
                }
            }
        }
        return true;
    }
}

相关推荐

  1. 代码随想-回溯

    2024-04-01 23:48:03       13 阅读
  2. 代码随想笔记

    2024-04-01 23:48:03       10 阅读
  3. 代码随想经历

    2024-04-01 23:48:03       5 阅读
  4. 代码随想回溯 |复原IP地址

    2024-04-01 23:48:03       36 阅读
  5. 代码随想回溯 |分割回文串

    2024-04-01 23:48:03       42 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-01 23:48:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-01 23:48:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-01 23:48:03       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-01 23:48:03       20 阅读

热门阅读

  1. FastAPI+React全栈开发17 让我们创建一个React应用

    2024-04-01 23:48:03       15 阅读
  2. 排序算法-选择排序

    2024-04-01 23:48:03       17 阅读
  3. ssh 启动 docker 中 app, docker logs 无日志

    2024-04-01 23:48:03       13 阅读
  4. 【华为OD机试C++】生成随机数

    2024-04-01 23:48:03       18 阅读
  5. Vue的数据为什么频繁变化但只会更新一次

    2024-04-01 23:48:03       14 阅读
  6. 基于Vue.js 实现简易拖拽指令

    2024-04-01 23:48:03       15 阅读
  7. C++引用详解

    2024-04-01 23:48:03       14 阅读