回溯算法补充leetcode

发布于:2025-04-09 ⋅ 阅读:(38) ⋅ 点赞:(0)

1. 组合

leetcode题目链接:77. 组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

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

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

解法:

class Solution {

    // 成员变量可以优化为函数入参
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> combine(int n, int k) {
        // 参数校验
        if(n <= 0 || k <= 0){
            return null;
        }

        // 回溯算法
        backTrack(n, k, 1);

        return result;        
    }

    public void backTrack(int n, int k, int startIndex){
        // 剪枝操作,剩余可选元素已经达不到k个
        // if(path.size() + n - startIndex + 1 < k){
        //     return;
        // }

        if(path.size() == k){
            // 注意,这里要值复制
            result.add(new ArrayList(path));
            System.out.println("debug: "+String.valueOf(path));
            return;
        }

        // 回溯算法的常规剪枝操作是在for循环里面控制i的最大值来实现
        for(int i = startIndex; i <= path.size() + n - k + 1; i++){
            path.add(i);
            // 注意,这里startIndex是从i+1开始的
            backTrack(n, k, i + 1);
            path.remove(path.size() - 1);
        }

    }
}

2. 组合总和III

leetcode题目链接:216. 组合总和III

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

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

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

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

解法

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

    public List<List<Integer>> combinationSum3(int k, int n) {
        if(k <= 0 || n <= 0){
            return null;
        }

        backTrack(k, n, 1);

        return result;  
    }

    /**
     * 递归方法
     */
    public void backTrack(int k, int n, int startIndex){
        if(path.size() == k && listSum(path) == n){
            result.add(new ArrayList(path));
            return;
        }

        if(path.size() == k){
            return;
        }

        // 剪枝1,i取值的最大值做限制,使得总的元素个数不会少于k个
        for(int i = startIndex; i <= 9 - (k-path.size()) + 1; i++){

            // 剪枝2,如果此时path中的元素和已经大于n了,提前结束for循环
            if(listSum(path) > n){
                return;
            }

            path.add(i);
            backTrack(k, n, i+1);
            path.remove(path.size()-1);
        }
    }

    /**
     * 求list中所有元素之和
     */
    public Integer listSum(List<Integer> path){
        Integer result = 0;
        for(Integer i : path){
            result += i;
        }

        return result;
    }

}

3. 组合总和II

leetcode题目链接:40. 组合总和II

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

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

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

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

解法:

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if(candidates == null || candidates.length == 0 || target <= 0){
            return null;
        }

        Arrays.sort(candidates);
        Boolean[] used = new Boolean[candidates.length];

        backTrack(candidates, target, 0, 0, used);
        return result;
    }

    public void backTrack(int[] candidates, int target, int sum, int startIndex, Boolean[] used){
        // 结果
        if(sum == target){
            result.add(new ArrayList(path));
            return; 
        }

        // 剪枝
        if(sum > target){
            return;
        }

        // 单次搜索
        for(int i = startIndex; i < candidates.length; i++){

            // 关键点!!!
            // 使用used数组记录元素使用状态,来判断值形同的元素是否是在同一层循环中,同一层循环只会选取一个元素所以前一个元素的used必定为0
            // 只有同一层循环,值相同的元素才需要去重,不同层不需要去重
            if(i > 0 && candidates[i] == candidates[i-1] && (used[i-1] == null || !used[i-1])){
                continue;
            }

            sum += candidates[i];
            path.add(candidates[i]);
            used[i] = true;
            backTrack(candidates, target, sum, i+1, used);
            sum -= candidates[i];
            path.remove(path.size() - 1);
            used[i] = false;
        }

    }

    public List<List<Integer>> combinationSum2_timeout(int[] candidates, int target) {
        if(candidates == null || candidates.length == 0 || target <= 0){
            return null;
        }

        Arrays.sort(candidates);

        backTrack(candidates, target, 0, 0);
        return result;
    }

    public void backTrack(int[] candidates, int target, int sum, int startIndex){
        // 结果
        if(sum == target){
            if(!listContain(result, path)){
                result.add(new ArrayList(path));
            }
            
            return; 
        }

        // 剪枝
        if(sum > target){
            return;
        }

        // 单次搜索
        for(int i = startIndex; i < candidates.length; i++){
            sum += candidates[i];
            path.add(candidates[i]);
            backTrack(candidates, target, sum, i+1);
            sum -= candidates[i];
            path.remove(path.size() - 1);
        }

    }

    /**
     * 判断list list 中是否包含了值相同的list(path)
     * 此解法会超时
     */
    public boolean listContain(List<List<Integer>> result, List<Integer> path){
        for(List<Integer> list : result){
            if(String.valueOf(list).equals(String.valueOf(path))){
                return true;
            }
        }

        return false;
    }
}

4. 复原IP地址

leetcode题目链接:93. 复原IP地址

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 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 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

解法:

class Solution {

    List<String> result = new ArrayList<>();
    List<String> path = new ArrayList<>();

    public List<String> restoreIpAddresses(String s) {
        if(s == null || s.length() == 0){
            return null;
        }

        backTrack(s, 0);
        return result;
    }

    public void backTrack(String s, int startIndex){
        
        // 递归到最后,并且刚好分成了4组合法的整数,这是要找的结果
        if(path.size() == 4 && startIndex == s.length()){ 
            result.add(String.join(".", path));
            return;
        }

        // 字符串遍历完了或者分割已经超过4组,后面不会有正确的结果了
        if(startIndex == s.length() || path.size() > 4){
            return;
        }

        // 单层搜索
        // 剪枝:i <= startIndex+3subString最长取3位就可以,取长了无意义
        for(int i = startIndex + 1; i <= s.length() && i <= startIndex+3; i++){
           String subString = s.substring(startIndex, i);
           if(valid(subString)){
                path.add(subString);
                backTrack(s, i);
                path.remove(path.size() - 1);
           }
        }

    }

    /**
     * 判断ip地址中的正数是否满足:每个整数位于 0 到 255 之间组成,且不能含有前导 0
     */
    public boolean valid(String subString){
        if(subString.startsWith("0") && subString.length() > 1){
            return false;
        }
        
        // 注意这里长度判断一下,否则字符串太长转换int/long会报错
        if(subString.length() > 3){
            return false;
        }

        Long subNum = Long.parseLong(subString);
        if(subNum >= 0 && subNum <= 255){
            return true;
        }

        return false;
    }
}

5. 子集II

leetcode题目链接:90. 子集II

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

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

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

解法:

class Solution {

    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
   
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if(nums == null || nums.length == 0){
            return null;
        }

        // 注意:因为要处理重复的元素,这里要先排好序,这样重复的元素必定会相邻
        Arrays.sort(nums);

        boolean[] used = new boolean[nums.length];
        backTrack(nums, 0, used);
        return result;        
    }

    public void backTrack(int[] nums, int startIndex,  boolean[] used){
        result.add(new ArrayList<>(path));

        for(int i = startIndex; i < nums.length; i++){

            // 注意:这里处理了同一层for循环中,如果元素值已经处理过了,这里continue。
            // 这里使用used数组来判定是不是同一层的for循环
            if(i > 0 && nums[i] == nums[i-1] && !used[i-1]){
                continue;
            }

            path.add(nums[i]);
            used[i] = true;
            backTrack(nums, i+1, used);
            path.remove(path.size() - 1);
            used[i] = false;

        }
    }
}

6. 非递减子序列

leetcode题目链接:491. 非递减子序列

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

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

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

输入:nums = [4,4,3,2,1]
输出:[[4,4]]

解法:

class Solution {

    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        if(nums == null || nums.length == 0){
            return null;
        }

        backTrack(nums, 0);
        return result;
    }

    public void backTrack(int[] nums, int startIndex){

        if(path.size() > 1){
            result.add(new ArrayList<>(path));
        }

        // 使用set记录同一层for循环中哪些元素已经遍历过,用于查找相同值的元素是否已经遍历过
        Set<Integer> set = new HashSet<>();
        for(int i = startIndex; i < nums.length; i++){

            // 注意:同一层for循环中,重复元素做去重操作
            if(set.contains(nums[i])){
                continue;
            }
            set.add(nums[i]);

            if(path.size() == 0 || path.get(path.size() - 1) <= nums[i]){
                path.add(nums[i]);
                backTrack(nums, i+1);
                path.remove(path.size() - 1);
            }

        }
    }
}

7. 全排列II

leetcode题目链接:47. 全排列II

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

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

示例 2:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

解法:

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

    public List<List<Integer>> permuteUnique(int[] nums) {
        if(nums == null || nums.length == 0){
            return null;
        }

        boolean[] used = new boolean[nums.length];
        backTrack(nums, used);

        return result;
    }

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

        // 注意:这里使用set记录同一层for循环选中的值,用于同一层for循环值相同的元素去重
        Set<Integer> set = new HashSet<>();
        for(int i = 0; i < nums.length; i++){

            // 去重
            if(set.contains(nums[i])){
                continue;
            }
            if(used[i]){
                continue;
            }

            // 真正选中的时候才add进set里面,上面continue的时候不add
            set.add(nums[i]);

            path.add(nums[i]);
            used[i] = true;
            backTrack(nums, used);
            path.remove(path.size() - 1);
            used[i] = false;

        }


    }
}

8. 解数独

leetcode题目链接:37. 解数独

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

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

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

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

示例 1:

输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

解法:

class Solution {
    public void solveSudoku(char[][] board) {

        backTrack(board);
        
    }

    // 注意:这个回溯只需要找到一个满足题意的结果就可以,不需要枚举校验所有的结果
    // 所以设置返回值,截断for循环提前结束递归,否则会超时
    public boolean backTrack(char[][] board){

        for(int i = 0; i < 9; i++){
            for(int j = 0; j < 9; j++){
                if(board[i][j] == '.'){
                    for(char num = '1'; num <= '9'; num++){
                        if(valid(board, i, j, num)){
                            board[i][j] = num;

                            // 在for循环里面return,直接截断剩余的循环
                            if(backTrack(board)){
                                return true;
                            };
                            board[i][j] = '.';
                        }
                    }

                    // 每个num都不合适,说明找不到正确结果了
                    return false;
                }               
            }
        }

        // 每一个i,j都填上了,说明九宫格填满了
        return true;
    }

    public boolean valid(char[][] board, int row, int col, char num){
        // 所在行没有num
        for(int i = 0; i < 9; i++){
            if(board[row][i] == num){
                return false;
            }
        }

        // 所在列没有num
        for(int i = 0; i < 9; i++){
            if(board[i][col] == num){
                return false;
            }
        }

        // 所在小方格没有num
        int startRow = row/3*3;
        int startCol = col/3*3;
        for(int i = startRow; i < startRow + 3; i++){
            for(int j = startCol; j < startCol + 3; j++){
                if(board[i][j] == num){
                    return false;
                }
            }
        }

        return true;
    }
}