【算法修炼】回溯专题一——组合、排列、子集、搜索

专题一涉及较多类型题目,主要是使用dfs配合for循环实现回溯,常见的题目:组合划分、子集划分、求组和、求子集、求排列、遍历所有情况的搜索、需要列出所有可能结果的搜索
学习自:https://programmercarl.com/0216.%E7%BB%84%E5%90%88%E6%80%BB%E5%92%8CIII.html

1、回溯模板

在这里插入图片描述
步骤:

  • 确定递归函数的参数和返回值
  • 回溯函数终止条件
  • 单层搜索过程

2、基础题目

2.1组合(中等)

zu
因为需要记录结果,所以必须得用回溯,如果只是统计个数可以用DP,题目中的限制条件很清楚就是1-n和k个数。

class Solution {
    // 存储最终答案
    List<List<Integer>> ans = new ArrayList<>();
    // 存储中间答案
    LinkedList<Integer> tmp = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        DFS(1, n, k);
        return ans;
    }
    public void DFS(int start, int n, int k) {
        if (tmp.size() == k) {
            // 个数达到要求,终止
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = start; i <= n; i++) {
            // 处理当前结点
            tmp.add(i);
            // 注意下一次开始的下标应该是i + 1
            DFS(i + 1, n, k);
            // 回溯,清除刚刚添加的结点
            tmp.removeLast();
        }
    }
}

在这里插入图片描述
一定要注意,for循环中的递归调用DFS(i + 1, n, k),是i + 1,而不是i,是为了避免[2,2]、[3,3]的这种重复情况。

2.2组合数(中等)

在这里插入图片描述
和上一题一样的解法,只是题目多了一个大小关系限制,我们从最大的值往下遍历即可。

import java.util.*;

public class Main {
    static LinkedList<Integer> ans = new LinkedList<>();
    public static void main(String[] args) {
        int n, r;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        r = scan.nextInt();
        DFS(n, r, n);
    }
    static void DFS(int n, int r, int start) {
        // 满足题目要求,打印
        if (ans.size() == r) {
            for (int i = 0; i < ans.size(); i++) {
                System.out.print(ans.get(i));
            }
            System.out.println();
            return;
        }
        // 按题目要求从大到小遍历
        for (int i = start; i >= 1; i--) {
            ans.add(i);
            DFS(n, r, i - 1);
            ans.removeLast();
        }
    }
}
2.3自然数拆分(中等)

在这里插入图片描述
一样的道理,但是本题可以重复使用数字,所以在递归调用的时候要注意参数!

import java.util.*;

public class Main {
    static LinkedList<Integer> ans = new LinkedList<>();
    public static void main(String[] args) {
        int n;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        DFS(n, 1, 0);
    }
    static void DFS(int n, int start, int sum) {
        if (sum > n) {
            return;
        }
        if (sum == n) {
            // 满足答案要求,打印
            System.out.printf("%d=", n);
            for (int i = 0; i < ans.size(); i++) {
                if (i == 0) {
                    System.out.print(ans.get(i));
                } else {
                    System.out.printf("+%d", ans.get(i));
                }
            }
            System.out.println();
            return;
        }
        for (int i = start; i < n; i++) {
            ans.add(i);
            // 本题目允许重复使用数字
            DFS(n, i, sum + i);
            ans.removeLast();
        }
    }
}
2.4数的划分(困难)

在这里插入图片描述
和上面题目一样的遍历方法,允许重复数字,下面是未剪枝的情况,会有两个例子超时。

import java.util.*;

public class Main {
    static int ans = 0;
//    static LinkedList<Integer> tmp = new LinkedList<>();
    public static void main(String[] args) {
        int n, k;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        k = scan.nextInt();
        // 每份至少分 1 ,所以下标得从1开始
        DFS(n, k, 1, 0, 0);
        System.out.println(ans);
    }
    static void DFS(int n, int k, int start, int cnt, int sum) {
        if (sum > n || cnt > k) {
            // 超出题目要求,提前返回
            return;
        }
        if (sum == n && cnt == k) {
            // 满足题目要求,累加答案
            ans++;
//            System.out.println(tmp);
            return;
        }
        for (int i = start; i <= n; i++) {
//            tmp.add(i);  // 方便调试
            // 题目允许重复使用数字,所以下次下标还可以从i 开始
            DFS(n, k, i, cnt + 1, sum + i);
//            tmp.removeLast();
        }
    }
}

剪枝,发现cnt是多余的,可以直接用m来计数,并且下面的for循环不是非得到n,只用到n - sum即可,画出搜索树就可以知道该怎么剪枝了。

import java.util.*;

public class Main {
    static int ans = 0;
    //    static LinkedList<Integer> tmp = new LinkedList<>();
    public static void main(String[] args) {
        int n, k;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        k = scan.nextInt();
        // 每份至少分 1 ,所以下标得从1开始
        DFS(n, k, 1, 0);
        System.out.println(ans);
    }
    static void DFS(int n, int k, int start, int sum) {
        if ((k == 0 && sum < n) || sum > n) {
            // 超出题目要求,提前返回
            return;
        }
        if (k == 0 && sum == n) {
            // 满足题目要求,累加答案
            ans++;
//            System.out.println(tmp);
            return;
        }
        for (int i = start; i <= n - sum; i++) {
//            tmp.add(i);  // 方便调试
            // 题目允许重复使用数字,所以下次下标还可以从i 开始
            DFS(n, k - 1, i, sum + i);
//            tmp.removeLast();
        }
    }
}

这道题其实也可以用DP解,但是很难想出状态转移方程,使用回溯很直接,加上剪枝也可以达到一样的效果,并且遇到需要打印出结果的题目,也可以做出解答。

2.5放苹果(困难)

在这里插入图片描述
本题是上题的改编,允许盘子为空。以题目为例,可能的情况为:007 016 025 034 115 124 133 223,可以画出搜索树:
在这里插入图片描述
从上图可以看到,0搜过之后开始结点还可以是0。一定要注意,做好剪枝,避免超时。

import java.util.*;

public class Main {
    static int ans = 0;
//    static LinkedList<Integer> tmp = new LinkedList<>();
    public static void main(String[] args) {
        int t;
        Scanner scan = new Scanner(System.in);
        t = scan.nextInt();
        int m, n;
        while(t > 0) {
            m = scan.nextInt();
            n = scan.nextInt();
            ans = 0;
            DFS(m, n, 0, 0);
            System.out.println(ans);
            t--;
        }
    }
    static void DFS(int m, int n, int start, int sum) {
        if (n == 0 && sum < m || sum > m) {
            return;
        }
//        if (n == 1 && sum == 0) {
//            System.out.println(tmp);
//            ans++;
//            return;
//        }
        if (n == 0 && sum == m) {
//            System.out.println(tmp);
            ans++;
            return;
        }
        for (int i = start; i <= m - sum; i++) {
//            tmp.add(i);
            DFS(m, n - 1, i, sum + i);
//            tmp.removeLast();
        }
    }
}
2.6合成分子(中等)

在这里插入图片描述
在这里插入图片描述
仔细分析题目,就相当于是把n拆分成10份,每份必须为1或2或3。并且题目要求必须打印出可能方案,就必须得搜索。

import java.util.*;

public class Main {
    static int ans = 0;
    static List<LinkedList<Integer>> ans_ = new ArrayList<>();
    static LinkedList<Integer> tmp = new LinkedList<>();
    public static void main(String[] args) {
        int n;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        // n 分成 10 份,每份必须为1、2、3份
        DFS(n, 0, 0);
        System.out.println(ans);
        for (int i = 0; i < ans_.size(); i++) {
            for (int j = 0; j < ans_.get(i).size(); j++) {
                if (j == 0) {
                    System.out.printf("%d", ans_.get(i).get(j));
                } else {
                    System.out.printf(" %d", ans_.get(i).get(j));
                }
            }
            System.out.println();
        }
    }
    static void DFS(int n, int cnt, int sum) {
        if (cnt > 10 || sum > n) {
            return;
        }
        if (cnt == 10 && sum == n) {
            ans++;
            ans_.add(new LinkedList<>(tmp));
            return;
        }
        //注意这里要从1-3遍历,才能满足题意
        for (int i = 1; i <= 3; i++) {
            tmp.add(i);
            DFS(n,cnt + 1, sum + i);
            tmp.removeLast();
        }
    }
}
2.7文具店(困难)

在这里插入图片描述
告诉了购买的水彩笔支数,那么就可以基于支数进行遍历,把字符串s拆分成三坨进行求和,找里面最小的和即可。

import java.util.*;

public class Main {
    static int ans = Integer.MAX_VALUE;
    // static LinkedList<Integer> test = new LinkedList<>();
    public static void main(String[] args) {
        String s = "";
        int k = 0;
        Scanner scan = new Scanner(System.in);
        s = scan.next();
        k = scan.nextInt();
        dfs(s, k, 0, 0, 0);
        System.out.println(ans);
    }
    static void dfs(String s, int k, int sum, int start, int len) {
        // 剪枝
        if (k == 0 && len < s.length() || len > s.length()) {
            return;
        }
        // 满足题目要求可以进行求最小值
        if (k == 0 && len == s.length()) {
            ans = Math.min(sum, ans);
            // System.out.println(test);
            return;
        }
        for (int i = start; i < s.length(); i++) {
            // 每次可能取7,72,725,7255,72553
            // 如果取了7,进入更深的dfs要从2开始取,2,25,255,2553
            // 如果不取7,那就继续往下取,取72,725,7255...直到遍历完全
            // 一定要保证最后遍历完了所有的字符
            String str = s.substring(start, i + 1);
            int tmp_len = str.length();
            int tmp = Integer.parseInt(str);
            // test.add(tmp);
            dfs(s, k - 1, sum + tmp, i + 1, len + tmp_len);
            // test.removeLast();
        }
    }
}
2.8奇怪的电梯(中等)

在这里插入图片描述
用dfs、bfs都可以,但需要对访问过的电梯楼层进行标记,因为在楼层跳转过程中很有可能又回到了之前已经遍历过的楼层,所以需要对访问过的楼层标记,不标记的话会陷入死循环。

对于每一层有两种操作,可以向上也可以向下,可以判断当前楼层能否向上、向下,再判断将要前往的楼层之前是否访问过了,本题与之前题目较大不同在于:需要标记访问过的楼层,因为这些访问过的楼层后续可能还访问到,重复了,而之前的题目都避免了重复访问的问题。

import java.util.*;

public class Main {
    static int ans = Integer.MAX_VALUE;
    static int[] dt = new int[220];
    static boolean[] vis = new boolean[220];
    public static void main(String[] args) {
        int n, a, b;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        a = scan.nextInt();
        b = scan.nextInt();
        for (int i = 1; i <= n; i++) {
            dt[i] = scan.nextInt();
        }
        // n层楼,从 a层到 b层要按几次按钮
        dfs(n, a, b, 0);
        System.out.println(ans == Integer.MAX_VALUE ? -1 : ans);
    }
    static void dfs(int n, int a, int b, int cnt) {
        if (a <= 0 || a > n) {
            return;
        }
        if (cnt >= ans) {
            return;
        }
        if (a == b) {
            ans = Math.min(ans, cnt);
            return;
        }
        vis[a] = true;
        // 可能向上
        if (a + dt[a] <= n && vis[a + dt[a]] == false) {
            dfs(n, a + dt[a], b, cnt + 1);
        }
        // 可能向下
        if (a - dt[a] >= 1 && vis[a - dt[a]] == false) {
            dfs(n, a - dt[a], b, cnt + 1);
        }
        // 回溯
        vis[a] = false;
    }
}
2.9分成互质组(困难)

在这里插入图片描述
互质:最大公约数 == 1,本题的难点在于要记录数属于哪个组,还需要把每个数与之前的组的每个数比较,如果互质的,那当前的数也放在那个组里;如果遍历完所有的已有组都不是互质的,那就需要新开一个组。整个逻辑比较绕,需要慢慢写。

import java.util.*;

public class Main {
    static int ans = Integer.MAX_VALUE;
    static int[] dt = new int[30];
    static boolean flag = false;
    // 记录每个元素组别
    static int[] group_s = new int[30];
    public static void main(String[] args) {
        int n;
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        for (int i = 1; i <= n; i++) {
            dt[i] = scan.nextInt();
        }
        // 互质:几个数的最大公约数 = 1
        // 从第一个数开始遍历,目前有1个组
        dfs(n, 1, 1);
        System.out.println(ans);
    }
    static void dfs(int n, int num, int group) {
        // num:现在遍历到第几个数,group:现在有几个组
        // num == n + 1,说明已经遍历完最后一个数
        if (num == n + 1) {
            ans = Math.min(ans, group);
        }
        if (num > n + 1) {
            return;
        }
        // 从第一组开始遍历,依次判断当前第num个数是否每个组都互质
        for (int i = 1; i <= group; i++) {
            flag = true;
            // 注意第一个数是进不了第二个循环的,因为第一个数可以直接放到第一个组里(第一个组暂时什么都没有)
            for (int j = 1; j < num; j++) {
                // gcd != 1 不是互质的,那说明不是同一组的
                if (group_s[j] == i && check(dt[j], dt[num]) != 1) {
                    flag = false;
                    break;
                }
            }
            // true说明当前数是已有的组里的
            if (flag == true) {
                // 保存组数
                group_s[num] = i;
                // 加数下标,不加组数
                dfs(n, num + 1, group);
            }
        }
        // false说明当前数不属于任何一个组,要新开一个组
        if (flag == false) {
            group_s[num] = group + 1;
            // 加数下标,加组数
            dfs(n, num + 1, group + 1);
        }
    }
    // 互质:最大公约数 = 1
    static int check(int a, int b) {
        if (b == 0) {
            return a;
        }
        return check(b, a % b);
    }
}
2.10组合总和(中等)

在这里插入图片描述
注意每个数可以重复用,求组合数(不强调顺序)

class Solution {
    LinkedList<Integer> tmp = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 不同组合,元素可以多次使用
        dfs(candidates, target, 0, 0);
        return ans;
    }
    public void dfs(int[] candidates, int target, int sum, int start) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            tmp.add(candidates[i]);
            // 注意下一次dfs的开始下标,从i开始
            dfs(candidates, target, sum + candidates[i], i);
            tmp.removeLast();
        }
    }
}

在这里插入图片描述
注意上面的dfs语句,只有让下一次开始的下标为上一次的 i 才能避免顺序不同,但实际是同一组和的情况。如果不记录开始下标,而是每次从第一个元素开始,则是求排列,两者比较如下图所示:
在这里插入图片描述
在这里插入图片描述

2.11组合总和Ⅱ(中等)

在这里插入图片描述
相比于上一道题,本题要求数组中的每个元素只能用一次,但是数组中的元素很有可能重复,例如 1 7 1组合成8,1 7 和 7 1都可以,这就重复了,怎么解决呢?

我们可以先对数组排序,排序了之后如果有重复的数,例如 1 7 1会排序为 1 1 7,这样我们只用考虑第一个1,遇到之后相等的情况就不要考虑了。

class Solution {
    LinkedList<Integer> tmp = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 不同组合,元素只能用一次,但是给出的元素又有重复的
        Arrays.sort(candidates);
        dfs(candidates, target, 0, 0);
        return ans;
    }
    public void dfs(int[] candidates, int target, int sum, int start) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = start; i < candidates.length; i++) {
            // 当数组中元素重复时,只考虑第一个重复元素
            if(i > start && candidates[i] == candidates[i-1])
                continue;
            tmp.add(candidates[i]);
            dfs(candidates, target, sum + candidates[i], i + 1);
            tmp.removeLast();
        }
    }
}
2.12组合总和Ⅲ(中等)

在这里插入图片描述
比之前的组合总和简单多了,数组中也没有重复的元素,直接遍历1-9,下一个dfs下标从i + 1开始即可。

class Solution {
    public List<List<Integer>> combinationSum3(int k, int n) {
        // 1 - 9的正整数
        dfs(k, n, 0, 1);
        return ans;
    }
    public LinkedList<Integer> tmp = new LinkedList<>();
    public List<List<Integer>> ans = new ArrayList<>();
    public void dfs(int k, int n, int sum, int start) {
        if (sum > n || k < 0) {
            return;
        }
        if (sum == n && k == 0) {
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = start; i <= 9; i++) {
            tmp.add(i);
            // 注意不允许有重复的数字
            dfs(k - 1, n, sum + i, i + 1);
            // 回溯
            tmp.removeLast();
        }
    }
}
2.13组合总和Ⅳ(中等)

在这里插入图片描述
这道题可以用dfs解,但是会超时,由于只需要返回种类数,考虑用dp解,就是之前在dp专题讲的爬楼梯问题,只不过这次每次可以爬的台阶数是nums中的各个数。

// 回溯:超时
class Solution {
    int ans = 0;
    public int combinationSum4(int[] nums, int target) {
        // 不同的数,组成target,求组合数,每个数可以用多次(112 121属于不同组合)
        dfs(nums, target, 0);
        return ans;
    }
    void dfs(int[] nums, int target, int sum) {
        if (sum > target) {
            return;
        }
        if (sum == target) {
            ans++;
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            dfs(nums, target, sum + nums[i]);
        }
    }
}
// 动态规划
class Solution {
    public int combinationSum4(int[] nums, int target) {
        int n = nums.length;
        // 类似于爬楼梯,但是每次可以走nums[i]的步数,需要target阶梯爬到楼顶
        // 问能爬到楼顶的方案数
        int[] dp = new int[target + 1];
        for (int i = 0; i < n; i++) {
            if (nums[i] <= target) {
                dp[nums[i]] = 1;
            }
        }
        // 第 i 个阶梯,可以由 i - nums[j] 的台阶 + nums[j]爬上来!
        for (int i = 1; i <= target; i++) {
            for (int j = 0; j < n; j++) {
                if (nums[j] <= i) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }
        return dp[target];
    }
}
2.14电话号码的字母组合(中等)

在这里插入图片描述
难点在于既要遍历按键,又要遍历按键内部的字符。不管它遍历几个东西,我们都得确定它们的排列顺序,显然是先确定某个按键,再确定该按键内某个字符。就跟上面互质组一样,对于某个数,要确定它和每一个组是不是互质,那不就是说先确定组,再确定组里的元素,这样就可以遍历了。

还需要注意本题中某个按键是否能重复按下,显然是不能的,所以在开始下一轮dfs迭代时也需要注意。

class Solution {
    HashMap<Integer, String> number = new HashMap<>();
    List<String> ans = new ArrayList<>();
    StringBuilder str = new StringBuilder();
    public List<String> letterCombinations(String digits) {
        if (digits.equals("")) {
            return new ArrayList<>();
        }
        number.put(2, "abc");
        number.put(3, "def");
        number.put(4, "ghi");
        number.put(5, "jkl");
        number.put(6, "mno");
        number.put(7, "pqrs");
        number.put(8, "tuv");
        number.put(9, "wxyz");
        // cnt代表遍历到第几个字符,inter代表内部遍历序号
        dfs(digits, 0, 0);
        return ans;
    }
    void dfs(String digits, int cnt, int start) {
        if (str.length() > digits.length()) {
            return;
        }
        if (str.length() == digits.length()) {
            ans.add(str.toString());
            System.out.println(str);
            return;
        }
        // 外层遍历按键数字,内层遍历每个数字内部的字母
        for (int i = start; i < digits.length(); i++) {
            String tmp = number.get(digits.charAt(i) - '0');
            for (int j = 0; j < tmp.length(); j++) {
                str.append(tmp.charAt(j));
                dfs(digits, cnt + 1, i + 1);
                // 回溯
                str.delete(str.length() - 1, str.length());
            }
        }
    }
}
2.15分割回文串(中等)

在这里插入图片描述
有了上面的题目经验,应该有了经验,回溯无非就是穷尽所有可能,但是在穷尽过程中需要根据题目要求去掉部分的可能,为了提升速度加上限制条件这就是剪枝。回到本题,就是尝试以1 2 3…的长度去划分字符串,再判断这些字符串是否为回文,是的话再次基础上再接着划分,注意要避免前面已经划分过的串。

class Solution {
    List<List<String>> ans = new ArrayList<>();
    LinkedList<String> tmp = new LinkedList<>();
    public List<List<String>> partition(String s) {
        // 把字符串s分割成多个子串,每个子串都是回文,输出可能的结果
        dfs(s, 0, 0);
        return ans;
    }
    void dfs(String s, int start, int cnt) {
        if (cnt > s.length()) {
            return;
        }
        if (cnt == s.length()) {
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = start; i < s.length() ; i++) {
            String str = s.substring(start, i + 1);
            // 截取一个子串出来看是不是回文
            if (check(str)) {
                tmp.add(str);
                // 注意下一次开始截取的字符串下标是多少,避免重复截取
                dfs(s, i + 1, cnt + str.length());
                tmp.removeLast();
            }
        }
    }
    // 判断当前子串是不是回文
    boolean check(String tmp) {
        int len = tmp.length();
        if (len <= 1) {
            return true;
        }
        for (int i = 0; i < len / 2; i++) {
            if (tmp.charAt(i) != tmp.charAt(len - i - 1)) {
                return false;
            }
        }
        return true;
    }
}
2.16复原IP地址(中等)

在这里插入图片描述
就是普通的搜索题目,注意下限制条件即可,可以通过统计点或者数的个数,来剪枝。

class Solution {
    List<String> ans = new ArrayList<>();
    String tmp = "";
    public List<String> restoreIpAddresses(String s) {
        // 无非就是4个数,3个点
        dfs(s, 0, 0, 0);
        return ans;
    }
    void dfs(String s, int num, int point, int start) {
        if (num == 4 && point == 3 && tmp.length() == s.length() + 3) {
            ans.add(tmp);
        }
        if (num > 4 || point > 3) {
            return;
        }
        // num :数字个数,point:点的个数
        for (int i = start; i < s.length(); i++) {
            String str = s.substring(start, i + 1);
            if (str.length() > 4) {
                continue;
            }
            int n = Integer.parseInt(str);
            if (str.length() != Integer.toString(n).length()) {
                // 有前置零
                continue;
            }
            if (n >= 0 && n <= 255) {
                String tmpp = tmp;
                if (num == 0) {
                    tmp = tmp + str;
                    dfs(s, num + 1, point, i + 1);
                } else {
                    tmp = tmp + "." + str;
                    dfs(s, num + 1, point + 1, i + 1);
                }
                // 回溯
                tmp = tmpp;
            }
        }
    }
}
2.17子集Ⅱ(中等)

在这里插入图片描述
数组中的元素可以重复了,如何处理重复元素?好像上面的题目有类似的吧,就是组合总和Ⅱ的方法,先排序,这样重复的元素都在一起了,对于重复的元素我们只用考虑它们的第一次的使用情况,例如:117,只考虑第一个1的使用情况:1 11 117,考虑了第一个就不考虑第二个了(如果再考虑是没有价值的,只会出现和前面的1重复的情况)

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    LinkedList<Integer> tmp = new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
    	// 排序处理元素重复的情况
        Arrays.sort(nums);
        dfs(nums, 0);
        return ans;
    }
    void dfs(int[] nums, int start) {
        ans.add(new ArrayList<>(tmp));
        for (int i = start; i < nums.length; i++) {
        	// 排除元素重复的使用
            if (i > start && nums[i - 1] == nums[i]) {
                continue;
            }
            tmp.add(nums[i]);
            dfs(nums, i + 1);
            tmp.removeLast();
        }
    }
}

有了上面组合总和Ⅱ和子集Ⅱ的例子,一定要学会处理数组中有重复元素,但是又得避免重复组合、子集的答案的处理方法:先排序,这样重复的元素就在一起了,只用考虑重复的元素一次即可。

2.18※递增子序列(中等)

在这里插入图片描述

下面的代码为什么不行了?

package Chapter_5;

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.findSubsequences(new int[] {1,2,3,4,5,6,7,8,9,10,1,1,1,1,1}));
    }
}
class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    LinkedList<Integer> tmp = new LinkedList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        // 递增子序列,元素不能重复使用,子序列中至少有两个元素
        dfs(nums,0);
        return ans;
    }
    void dfs(int[] nums, int start) {
        if (tmp.size() > nums.length) {
            return;
        }
        if (tmp.size() >= 2) {
            ans.add(new ArrayList<>(tmp));
        }
        for (int i = start; i < nums.length; i++) {
            if (i > start && nums[i] == nums[i - 1]) {
                return;
            }
            if (tmp.size() == 0) {
                tmp.add(nums[i]);
                dfs (nums, i + 1);
                tmp.removeLast();
            } else {
                if (nums[i] >= tmp.get(tmp.size() - 1)) {
                    tmp.add(nums[i]);
                    dfs (nums, i + 1);
                    tmp.removeLast();
                }
            }
        }
    }
}

很明显可以看到这样一个例子:1,2,3,4,5,6,7,8,9,10,1,1,1,1,1,前面的1考虑后,并没有过滤掉后面重复的1。排序再去重呢?按照题目意思,如果排序就改变题目意思了,题目是说在原始的nums数组中进行寻找。所以为了避免后面的又被重复考虑了,只能把用过的数存入set,避免后续的重复考虑。(注意set的声明位置!!!当然也可以对结果去重)

class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    LinkedList<Integer> tmp = new LinkedList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        // 递增子序列,元素不能重复使用,子序列中至少有两个元素
        dfs(nums,0);
        return ans;
    }
    void dfs(int[] nums, int start) {
        if (tmp.size() > nums.length) {
            return;
        }
        if (tmp.size() >= 2) {
            ans.add(new ArrayList<>(tmp));
        }
        // 注意set的声明位置,只用排除每一层的重复元素
        // 因为一层只应该考虑相同的元素一次
        Set<Integer> used = new HashSet<>();
        for (int i = start; i < nums.length; i++) {
            if (used.contains(nums[i])) {
                continue;
            }
            if (tmp.size() == 0) {
                used.add(nums[i]);
                tmp.add(nums[i]);
                dfs (nums, i + 1);
                tmp.removeLast();
            } else {
                if (nums[i] >= tmp.get(tmp.size() - 1)) {
                    used.add(nums[i]);
                    tmp.add(nums[i]);
                    dfs (nums, i + 1);
                    tmp.removeLast();
                }
            }
        }
    }
}
2.19全排列(中等)

在这里插入图片描述
之前一直都是组合问题,没有涉及排列问题,以输入:1 2 3为例,如何输出2 1 3,在之前组合类题目中,确定了2之后不会再遍历到2之前的元素了,所以无论在哪一层都应该从第一个元素开始遍历。但是重复问题如何解决?用一个数组记录当前遍历过程中用过的元素即可,在回溯的过程中把用过的元素再清0即可(保证后面可以再次遍历到它)。

class Solution {
    LinkedList<Integer> tmp = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    int[] vis = new int[30];
    public List<List<Integer>> permute(int[] nums) {
        dfs(nums);
        return ans;
    }
    void dfs(int[] nums) {
        if (tmp.size() == nums.length) {
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (vis[i] == 1) {
                continue;
            }
            // 访问过的元素标记
            vis[i] = 1;
            tmp.add(nums[i]);
            dfs(nums);
            // 回溯
            vis[i] = 0;
            tmp.removeLast();
        }
    }
}
2.20※全排列Ⅱ(中等)

在这里插入图片描述
上面记录每个元素是否访问肯定是无法使用了,因为有1 1 2这种情况出现。可以先排序,和之前找子集一样的去重方法,对于1 1 2,要避免第二个“1”的重复结果,如果第一个1已被访问,说明当前的情况是1重新被遍历,因为为了构成排列,每次遍历都从第一个元素开始,就导致了结果的重复。(可以画图看看如何避免重复的情况)

class Solution {
    LinkedList<Integer> tmp = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    int[] vis = new int[30];
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        dfs(nums);
        return ans;
    }
    void dfs(int[] nums) {
        if (tmp.size() == nums.length) {
            ans.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (vis[i] == 1) {
                continue;
            }
            // 避免重复元素的重复结果
            if (i > 0 && nums[i] == nums[i - 1] && vis[i - 1] == 0) {
                continue;
            }
            vis[i] = 1;
            tmp.add(nums[i]);
            dfs(nums);
            // 回溯
            vis[i] = 0;
            tmp.removeLast();
        }
    }
}

上面几道题其实模板都是一样的,区别是:每个元素能否重复使用、是否需要按照数组顺序、是否需要考虑顺序、考虑的是组合还是排列。较难的地方就是去重,需要想清楚应该哪里去重能够优化时间(相当于是剪枝,因为这些问题其实都可以对结果去重,只是时间花销大),一般可以通过画搜索树观察结构进行剪枝,一定要分清楚每层和分枝的关系。

for (int i = start; i < n; i++) {
	tmp.add(i);
	dfs(nums, i + 1);
	tmp.removeLast();
}

上面的for循环如果不管dfs语句,那就是在遍历一层,如果一直只考虑dfs语句,一直深入,那就是在遍历当前分枝到尽头。

2.21字母大小写全排列(中等)

在这里插入图片描述
这道题简单,只用管字母,把小写大写互相转换,遍历完所有情况就可以。关键是要会使用String和char[],String可以直接转成char[],char[]也可以通过new String()生成String,用char[]的好处是可以直接通过下标修改字符,修改完了回溯也方便。

class Solution {
    List<String> ans = new ArrayList<>();
    public List<String> letterCasePermutation(String s) {
    	// String也是可以直接转成char[]的
        char[] chs = s.toCharArray();
        dfs(chs, 0);
        return ans;
    }
    void dfs(char[] chs, int start) {
    	// char[]数组是可以生产String的
        ans.add(new String(chs));
        for (int i = start; i < chs.length; i++) {
            char tmp = chs[i];
            if (tmp >= '0' && tmp <= '9') {
                continue;
            } else if (tmp >= 'a' && tmp <= 'z') {
                chs[i] = (char) (chs[i] - 32);
            } else if (chs[i] >= 'A' && chs[i] <= 'Z') {
                chs[i] = (char) (chs[i] + 32);
            }
            dfs(chs, i + 1);
            chs[i] = tmp;
        }
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@u@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值