【力扣刷题】Day22——回溯专题

文章目录

  • 回溯专题
    • 递归实现指数型枚举
    • 递归实现排列型枚举
    • 递归实现组合型枚举
    • 一、组合问题
      • 1. 组合I
      • 2. 组合总和
      • 3. 组合总和II
      • 4. 组合总和III
      • 5.组合总和IV(TODO)
      • 6. 话号码的字母组合


回溯专题

回溯法介绍:

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。

回溯是递归的副产品,只要有递归就会有回溯。

回溯法解决问题:

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

排列与组合的区别:

组合是不强调元素顺序的,排列是强调元素顺序

例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了

递归实现指数型枚举

题目:从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。

指数类型枚举:每一个数只有两种选择,选或者不选

代码模板:

boolean[] st;
dfs(1);// 从1开始搜索

// 这里的u枚举的是数,而不是填数的位置
private static void dfs(int u) {
        if(u > n){// 递归出口
            for(int i = 1; i <= n; i ++){
                if(st[i] == true)
                    System.out.print(i + " ");
            }
            System.out.println();
            return ;
        }
        
        // 每个数选或者不选
        
        st[u] = true;
        dfs(u + 1);
        
        st[u] = false;
        dfs(u + 1);
    }

递归实现排列型枚举

题目:求n的排列(n <= 10)

代码模板:

int[] path;
boolean[] st;
dfs(1);// 从第一个位置开始填数


public static void dfs(int u){
        if(u > n){// 递归出口
            for(int i = 1; i <= n; i ++){// 处理逻辑
                System.out.print(path[i] + " ");
            }
            System.out.println();
            return ;
        }
        // 枚举每一种可能
        for(int i = 1; i <= n; i ++){
            if(!st[i]){
                st[i] = true;
                path[u] = i;
                dfs(u + 1);
                st[i] = false;
                path[u] = 0;
            }
        }
    }

递归实现组合型枚举

题目:从1~n的数中选出m个数,有多少种可能

不考虑顺序的枚举,是组合型枚举,eg:123和213是一种方案;而在排列型枚举中是属于不同方案

如何实现组合类型枚举:

限制后面位置要放的数字比前一个位置要放的数字大,就可以满足不重复,实现了去重。

相比于排列模板,组合规定了一个数选择的顺序

代码模板:

int[] ways;
dfs(0, 1);// 从第0个位置开始填,从1开始搜

private static void dfs(int u, int start) {
        if(u == m){
            for(int i = 0; i < m; i ++){
                System.out.print(ways[i] + " ");
            }
            System.out.println();
            return ;
        }
        // 规定顺序,枚举每一种可能
        for(int i = start; i <= n; i ++){
            ways[u] = i;
            dfs(u + 1, i + 1);
            ways[u] = 0;// 回溯 恢复现场
        }
    }

一、组合问题

1. 组合I

题目链接:77. 组合 - 力扣(LeetCode)

Code

class Solution {
    static int n, m;
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combine(int n1, int m1) {
        n = n1; m = m1;
        dfs(0, 1);// 从第0个位置开始填数,从1开始搜索
        return res;
    }
    public void dfs(int u, int start){
        if(u == m){
            res.add(new ArrayList(path));
            return ;
        }
        for(int i = start; i <= n; i ++){
            path.add(i);
            dfs(u + 1, i + 1);
            path.remove(path.size() - 1);
        }
    }
}

2. 组合总和

题目链接:39. 组合总和 - 力扣(LeetCode)

序列是无重复的!

递归枚举组合类型:本题不限定选多少个数,也不限定每一个数选择的次数,为此我们枚举的时候,下一次还是从自己开始,某一个数能不能选关键在于nums[i]加入path的条件是sum + nums[i] <= target否。

如果至少一个数字的被选数量不同,则两种组合是不同的。 ------ 说明组合是不能够重复的(2 2 3 和 3 2 2是同一种)

Code

/**
    组合模板的变形,同一个数可以取无限次:
        枚举时要是当前数比targetget大了,说明该数不能要了直接break;
 */
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int target;
    public List<List<Integer>> combinationSum(int[] nums, int val) {
        target = val;
        Arrays.sort(nums);
        dfs(nums, 0, 0);// 从第0位置的数开始枚举
        return res;
    }
    public void dfs(int[] nums, int start, int sum){
        // 满足条件的递归出口
        if(sum == target){
            res.add(new ArrayList(path));
            return ;
        }
        
        // 按一定的顺序枚举每一种可能
        for(int i = start; i < nums.length && nums[i] + sum <= target; i ++){
            path.add(nums[i]);
            dfs(nums, i, sum + nums[i]);// 下一次还从i开始(无限次嘛)
            path.remove(path.size() - 1);
        }
    }
}

另一种写法:

/**
    组合模板的变形,同一个数可以取无限次:
        枚举时要是当前数比target大了,说明该数不能要了直接break;
 */
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] nums, int target) {
        Arrays.sort(nums);
        dfs(nums, 0, target);// 从第0位置的数开始枚举
        return res;
    }
    public void dfs(int[] nums, int start, int target){
        // 满足条件的递归出口
        if(target == 0){
            res.add(new ArrayList(path));
            return ;
        }
        
        // 按一定的顺序枚举每一种可能
        for(int i = start; i < nums.length; i ++){
            if(nums[i] > target){// 若nums[i] > targrt说明无法再凑够target
                break;
            }
            path.add(nums[i]);
            dfs(nums, i, target - nums[i]);// 下一次还从i开始(无限次嘛)
            path.remove(path.size() - 1);
        }
    }
}

3. 组合总和II

题目链接:40. 组合总和 II - 力扣(LeetCode)

本题和上一题的区别是,上一题每一个数可以取无限个且序列无重复,而本题每一个组合种每一个数只能使用一次且序列可能重复!

由于本题序列可能重复,那么我们枚举组合的时候难免会出现重复集合,那我们如何去重?———— 如何确定枚举顺序?

  • 先排序,让重复的元素都聚在一起
  • 当我们枚举组合时,每一个数只能枚举一次,遇到重复的就跳过即可。
  • 整体思路跟39题大同小异

Code

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int target;
    public List<List<Integer>> combinationSum2(int[] nums, int val) {
        target = val;
        Arrays.sort(nums);
        dfs(nums, 0, 0);
        return res;
    }
    public void dfs(int[] nums, int start, int sum){
        if(sum == target){
            res.add(new ArrayList(path));
            return ;
        }

        for(int i = start; i < nums.length && sum + nums[i] <= target; i ++){
            if(i > start && nums[i] == nums[i - 1]){// 保证每一个数只能选一次
                continue;
            }
            path.add(nums[i]);
            dfs(nums, i + 1, sum + nums[i]);// 下一次从i + 1开始
            path.remove(path.size() - 1);
        }
    }
}

4. 组合总和III

题目链接:216. 组合总和 III - 力扣(LeetCode)

本题和组合总和II的相同点是每一个只能使用一次,不同点是每一个答案集(组合)大个数(大小)是限定的,限定为k个数。那么我们就要在枚举的时候做出相应改变,进行适当的剪枝

  • sum == n时,选择数的个数未达到k,即不符合
  • sum 未到达 n 之前,选择数的个数已经超过了k,即也不符合

Code

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int targrt;
    int k;
    public List<List<Integer>> combinationSum3(int m, int n) {
        targrt = n;
        k = m;
        dfs(1, 0, 0);
        return res;
    }
    public void dfs(int start, int sum, int cnt){
        if(cnt == k && sum == targrt){
            res.add(new ArrayList(path));
            return ;
        }
        // 剪枝
        if(cnt > k || sum > targrt){
            return ;
        }
        // 枚举所有可能组合
        for(int i = start; i <= 9 && sum + i <= targrt; i ++){
            path.add(i);
            dfs(i + 1, sum + i, cnt + 1);
            path.remove(path.size() - 1);
        }
    }
}

5.组合总和IV(TODO)

这题和组合总和I的联系和区别,都是无限次,但组合可以重复([1,1,2][1,2,1][2,1,1])都是一个合理答案————也就是说答案可以重复(更新排列类型)

  • 我们每一次枚举的时候,都可以重新开始!

dfs写法在组合总和I代码基础上改了改————超时了(数据量比组合总和I还大)

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int target;
    int ans = 0;
    public int combinationSum4(int[] nums, int val) {
        target = val;
        Arrays.sort(nums);
        dfs(nums, 0, 0);// 从第0位置的数开始枚举
        return ans;
    }
    public void dfs(int[] nums, int start, int sum){
        // 满足条件的递归出口
        if(sum == target){
            ans ++;
            res.add(new ArrayList(path));
            return ;
        }
        // 从0开始
        for(int i = 0; i < nums.length && nums[i] + sum <= target; i ++){
            path.add(nums[i]);
            dfs(nums, i, sum + nums[i]);// 既然可以重复,每一次都可以从第0个位置的数枚举
            path.remove(path.size() - 1);
        }
    }
}

本题的正解应该是DP,后续刷到动态规划专题再来补一补吧

6. 话号码的字母组合

题目链接:17. 电话号码的字母组合 - 力扣(LeetCode)

Code

思路一:迭代

【力扣刷题】Day22——回溯专题_第1张图片

class Solution {

    static Map<String, String> mp = new HashMap<>(){
        {
            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> ans = new ArrayList<>();
        int n = digits.length();
        if(n == 0) return ans;
        ans.add("");

        for(int i = 0; i < n; i ++){
            // 存放到变化后的数组
            List<String> t = new ArrayList<>();
            String num = digits.substring(i, i + 1);
            String s = mp.get(num);
            for(int j = 0; j < s.length(); j ++){// 枚举数字对应的所有字母
                String e = s.substring(j, j + 1);
                // 遍历旧链表存放的组合,并将该字母拼接到所有组合的后面
                for(String x : ans){
                    t.add(x + e);
                }
            }
            ans = t;// 更新组合状态
        }
        return ans;
    }
}

思路二:dfs + 回溯

class Solution {

    static Map<String, String> mp = new HashMap<>(){
        {
            put("2", "abc");
            put("3", "def");
            put("4", "ghi");
            put("5", "jkl");
            put("6", "mno");
            put("7", "pqrs");
            put("8", "tuv");
            put("9", "wxyz");
        }
    };

    static List<String> ans = new ArrayList<>();
    static void dfs(String digits, int u, String path){
        if(u == digits.length()){
            ans.add(path);
            return ;
        }
        String t = mp.get(digits.substring(u, u + 1));
        for(int i = 0; i < t.length(); i ++){
            dfs(digits, u + 1, path + t.charAt(i));
        }
    }

    public List<String> letterCombinations(String digits) {
        ans.clear();
        int n = digits.length();
        if(n == 0) return ans;
        dfs(digits, 0, "");
        return ans;
    }
}

你可能感兴趣的:(代码随想录力扣刷题,leetcode,深度优先,算法)