回溯--组合,排列

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
在这里插入图片描述

思路

排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。
排列问题需要一个used数组标记已经选择的元素,如图橘黄色部分所示:
在这里插入图片描述
排列问题的不同:
1.每层都是从0开始搜索而不是startIndex
2.需要used数组记录path里都放了哪些元素了

代码

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];
        backtracing(nums,used);
        return res;

    }
    public void backtracing(int[] nums,boolean[] used){
        if(path.size() == nums.length){
            res.add(new ArrayList<Integer>(path));
            return;       
        }
        for(int i = 0; i < nums.length; i++){
            if(used[i] == true)  continue;
            path.add(nums[i]);
            used[i] = true;
            backtracing(nums,used);
            used[i] = false;
            path.removeLast();
        }
        
    }
}

47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
在这里插入图片描述

思路

涉及到去重
还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。

思路

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] used;
    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        used = new boolean[nums.length];
        backtracing(nums);
        return res;

    }

    public void backtracing(int[] nums){
        if(path.size() == nums.length){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i = 0; i < nums.length; i++){
            //去重
            if(i > 0 && nums[i] == nums[i-1] && used[i-1] == true)
                continue;
            if(used[i] == true)
                continue;
            path.add(nums[i]);
            used[i] = true;
            backtracing(nums);
            used[i] = false;
            path.removeLast();
        }
    }
}

面试题 08.07. 无重复字符串的排列组合

无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。
在这里插入图片描述

代码

class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder path = new StringBuilder();
    boolean[] used;
    public String[] permutation(String S) {
        char[] arr = S.toCharArray();
        used = new boolean[arr.length];
        backtracing(arr);
        return res.toArray(new String[res.size()]);

    }
    //确定回溯参数
    public void backtracing(char[] arr){
        //终止条件
        if(path.length() == arr.length){
            res.add(path.toString());
            return;
        }

        for(int i = 0; i < arr.length; i++){
            if(used[i] == true)  continue;
            //标记为已访问过
            used[i] = true;

            path.append(arr[i]);
            backtracing(arr);

            used[i] = false;
            path.deleteCharAt(path.length() - 1);
        }
    }
}

面试题 08.08. 有重复字符串的排列组合

有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。
在这里插入图片描述

代码
class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder path = new StringBuilder();
    //排列问题一定要有标记数组
    boolean[] used;
    public String[] permutation(String S) {
        //去重一定要对数组进行排序
        char[] arr = S.toCharArray();
        Arrays.sort(arr);
        used = new boolean[arr.length];
        backtracing(arr);
        return res.toArray(new String[res.size()]);

    }
    public void backtracing(char[] arr){
        if(path.length() == arr.length){
            res.add(path.toString());
            return;
        }

        for(int i = 0; i < arr.length; i++){
            //去重操作
            if(i > 0 && arr[i] == arr[i-1] && used[i-1] == false){
                continue;
            }
            if(used[i] == true){
                continue;
            }
            used[i] = true;

            path.append(arr[i]);
            backtracing(arr);

            used[i] = false;
            path.deleteCharAt(path.length() - 1);
        }
    }
}

剑指 Offer 38. 字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
在这里插入图片描述

同上一题
代码
class Solution {
    StringBuilder path = new StringBuilder();
    List<String> res = new ArrayList<>();
    boolean[] used;
    public String[] permutation(String s) {
        //全排列 + 回溯 
        //要考虑有重复元素的情况---去重
        used = new boolean[s.length()];
        char[] arr = s.toCharArray();
        Arrays.sort(arr);
        backtracking(arr);
        return res.toArray(new String[res.size()]);
    }

    public void backtracking(char[] arr){
        if(path.length() == arr.length){
            res.add(path.toString());
            return;
        }

        for(int i = 0; i < arr.length; i++){
            if(i > 0 && arr[i] == arr[i-1] && used[i - 1] == false){
                continue;
            }
            if(used[i] == false){
                used[i] = true;
                path.append(arr[i]);
                backtracking(arr);
                path.deleteCharAt(path.length()-1);
                used[i] = false;
            }
            
        }
    }
}

77. 组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
在这里插入图片描述

思路

在这里插入图片描述
函数里一定有两个参数,既然是集合n里面取k的数,那么n和k是两个int型的参数。然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,…,n] )。
为什么要有这个startIndex呢?
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex。

void backtracking(int n, int k, int startIndex) 

剪枝优化
可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
优化过程:

  1. 已经选择的元素个数:path.size();
  2. 还需要的元素个数为: k - path.size();
  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
代码
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combine(int n, int k) {
        backtracing(n,k,1);
        return res;
    }

    public void backtracing(int n,int k,int startIndex){
        if(k == path.size()){
            res.add(new ArrayList<Integer>(path));
            return;
        }

        for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){
            path.add(i);
            backtracing(n,k,i+1);
            path.removeLast();
        }
   
    }
}

216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9;
每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
在这里插入图片描述

思路

在这里插入图片描述
剪枝操作:
1.已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。

if (sum > targetSum) { // 剪枝操作
    return;
}

2.for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。

代码
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> combinationSum3(int k, int n) {
        backtracing(k,n,1,0);
        return res;

    }

    public void backtracing(int k,int n,int startIndex,int sum){
        if(sum > n){
            return;
        }
        //终止条件
        if(path.size() == k){
            if(sum == n){
                res.add(new ArrayList<>(path));
            }
            return;
        }

        for(int i = startIndex; i <= 9-(k-path.size())+1; i++){
            path.add(i);
            sum += i;
            backtracing(k,n,i+1,sum);
            sum -= i;
            path.removeLast();
        }

    }      
    
}

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
请添加图片描述
在这里插入图片描述

代码
class Solution {
    //本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而77. 组合 和216.组合总和III都是是求同一个集合中的组合!
    List<String> res = new ArrayList<>();
    StringBuilder path = new StringBuilder();
    public List<String> letterCombinations(String digits) {
        if(digits == null || digits.length() == 0)
            return res;
        String[] dig = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
        backtracing(digits,dig,0);
        return res; 
    }
    //这个num是记录遍历第几个数字了,就是用来遍历digits的,同时num也表示树的深度。
    public void backtracing(String digits,String[] dig,int num){
        //输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。那么终止条件就是如果num 等于 输入的数字个数(digits.size)了(本来num就是用来遍历digits的)
        if(num == digits.length()){
            res.add(path.toString());
            return;
        }
        //首先要取num指向的数字,并找到对应的字符集(手机键盘的字符集)。
        String str = dig[digits.charAt(num)-'0'];

        for(int i = 0; i < str.length(); i++){
            path.append(str.charAt(i));
            // 递归,注意sum+1,一下层要处理下一个数字了
            backtracing(digits,dig,num+1);
            path.deleteCharAt(path.length()-1);
        }


    }
}

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个
在这里插入图片描述

思路

在这里插入图片描述1.递归函数参数
2.递归终止条件 sum大于target和sum等于target。
3.单层搜索的逻辑
单层for循环依然是从startIndex开始,搜索candidates集合。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

如果是一个集合来求组合的话,就需要startIndex
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,

代码

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backtracing(candidates,target,0);
        return res;

    }

    public void backtracing(int[] candidates,int target,int startIndex){
        if(sum > target){
            return;
        }
        if(sum == target){
            res.add(new ArrayList<Integer>(path));
            return;
        }

        for(int i = startIndex; i < candidates.length; i++){
            sum += candidates[i];
            path.add(candidates[i]);
            backtracing(candidates,target,i);
            sum -= candidates[i];
            path.removeLast();
        }
    }
}

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次
注意:解集不能包含重复的组合。
在这里插入图片描述

思路

1.本题candidates 中的每个数字在每个组合中只能使用一次。
2.本题数组candidates的元素是有重复的,而39.组合总和是无重复元素的数组candidates
本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合。所以要在搜索的过程中就去掉重复组合。
元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。

树层去重的话,需要对数组排序!

在这里插入图片描述
1.递归函数参数
2.递归终止条件
3.单层搜索的逻辑

代码
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        backtracing(candidates,target,0);
        return res;

    }
    public void backtracing(int[] candidates, int target,int startIndex){
        if(sum > target){
            return;
        }
        if(sum == target){
            res.add(new ArrayList<Integer>(path));
            return;
        }
        for(int i = startIndex; i < candidates.length; i++){
            //去重操作
            if(i > startIndex && candidates[i] == candidates[i-1]){
                continue;
            }
            sum += candidates[i];
            path.add(candidates[i]);
            backtracing(candidates,target,i+1);
            sum -= candidates[i];
            path.removeLast();
        }
    }
}

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
在这里插入图片描述

思路

组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
在这里插入图片描述
遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合。

  1. 递归函数参数
  2. 递归终止条件
    剩余集合为空的时候,就是叶子节点。
    那么什么时候剩余集合为空呢?
    就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了
  3. 单层搜索逻辑
  4. 求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树。
代码
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backtracing(nums,0);
        return res;

    }
    public void backtracing(int[] nums,int startIndex){
        res.add(new ArrayList<Integer>(path));
        if(startIndex >= nums.length){
            return;
        }
        for(int i = startIndex; i < nums.length; i++){
            path.add(nums[i]);
            backtracing(nums,i+1);
            path.removeLast();
        }
    }
}

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
在这里插入图片描述

!!! 去重
代码
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        backtracing(nums,0);
        return res;

    }
    public void backtracing(int[] nums,int startIndex){
        res.add(new ArrayList<Integer>(path));
        for(int i = startIndex; i < nums.length; i++){
            if(i > startIndex && nums[i] == nums[i-1]){
                continue;
            }
            path.add(nums[i]);
            backtracing(nums,i+1);
            path.removeLast();
        }
    }
}

剑指 Offer 34. 二叉树中和为某一值的路径

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
在这里插入图片描述

代码
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    LinkedList<List<Integer>> res = new LinkedList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int target) {
        if(root == null)  return res;
        dfs(root,target);
        return res;
    }

    public void dfs(TreeNode root,int target){
        if(root == null)  return;
        path.add(root.val);
        target -= root.val;
        //1.各节点值的和等于目标值target时 2.根节点到叶子节点形成的路径,将此路径加入列表结果
        if(target == 0 && root.left == null && root.right == null)
        //new LinkedList(path)相当于复制了一份path到res中,因为后面path会发生变化,所以不能写成res.add(path).
            res.add(new LinkedList(path));
        //先序遍历 根左右
        dfs(root.left,target);
        dfs(root.right,target);

        //回溯
        path.removeLast();     
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值