首页 > 其他分享 >子集与全排列问题(力扣78,90,46,47)

子集与全排列问题(力扣78,90,46,47)

时间:2024-04-02 21:59:20浏览次数:26  
标签:used nums 46 47 力扣 int result new path

系列文章目录

子集和全排列问题与下面的组合都是属于回溯方法里的,相信结合前两期,再看这篇笔记,更有助于大家对本系列的理解

一、组合回溯问题
二、组合总和问题


文章目录


题目

Problem: 78. 子集
Problem: 90. 子集 II
Problem: 46. 全排列
Problem: 47. 全排列 II

子集II与全排列II都只是在前面的基础上加了题目所给的nums数组里的元素可重复这个条件
子集问题
给你一个整数数组 nums ,数组中的元素互不相同 。返回该数组所有可能的
子集(幂集)。解集 不能包含重复的子集。你可以按任意顺序返回解集。

示例 1:

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

示例 2:

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

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

示例 1:

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

示例 2:

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

示例 3:

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


子集

一、思路

与组合和组合总和问题不同的是每遍历到一个树的结点,都要把结果加入到result结果集里面,而不是到叶子结点时,才收集结果

例:从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合
在这里插入图片描述

二、解题方法

回溯三部曲

  1. 递归函数的返回值以及参数:
    参数为startIndex和nums整数数组
 public void backTracking(int[] nums,int startIndex)
  1. 回溯函数终止条件:
    到叶子结点时就退出递归,去遍历其它路径上的结果
if(startIndex >= nums.length) {
            return;
        }
  1. 单层搜索的过程:
    每遍历一次都把元素添加到路径path中,递归回溯,每次递归startIndex都从后一位递归,否则会造成重复的组合。
for(int i = startIndex;i<nums.length;i++) {
            path.add(nums[i]);
            backTracking(nums,i+1);
            path.removeLast();
        }

在每层递归函数里,都要把当前路径添加到result结果集中,要写在终止条件之前,否则退出之后,当前结果还没有保存就回溯其它路径上的结果了。

result.add(new ArrayList<>(path));

优化:在这里其实可以省略掉终止条件,因为就算遍历超过叶子结点了,不满足for循环的条件里,for循环里面也不会执行,就直接return了。

三、Code

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

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

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

子集II

一、思路

这道题与子集I的区别在于题目给的数组元素可重复,这就要对组合结果去重,与组合总和II是相同的套路,都是先排序之后,再判断前面同一层是否出现了相同的访问过的元素,如果出现了则直接跳过这次循环

在这里插入图片描述

二、解题方法

与上面的子集问题I多了先对nums数组进行排序,在for循环内对横向遍历去重,去重的方法和组合总和II是一样的有以下两种:

  • used数组:
    如果前面的元素和当前元素相同,并且i>0,前面的元素没有使用过,说明不是同一条路径下使用过的的元素,而是同一层内的
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
                continue;
            }
  • startIndex:
    如果前面的元素和当前元素相同,并且startIndex已经不是第一个开始遍历的,则直接跳过这次循环
if(i > startIndex && nums[i] == nums[i-1]){
                continue;
            }

三、Code

used数组

class Solution {
   List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
   LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
   boolean[] used;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        used = new boolean[nums.length];
        subsetsWithDupHelper(nums, 0);
        return result;
    }
    
    private void subsetsWithDupHelper(int[] nums, int startIndex){
        result.add(new ArrayList<>(path));

        if (startIndex >= nums.length){
            return;
        }

        for (int i = startIndex; i < nums.length; i++){
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
                continue;
            }

            path.add(nums[i]);
            used[i] = true;
            subsetsWithDupHelper(nums, i + 1);
            path.removeLast();
            used[i] = false;
        }
    }
}

startIndex

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

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        backTracking(nums,0);
        return result;
    }

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

        if(startIndex >= nums.length) {
            return;
        }

        for(int i = startIndex;i<nums.length;i++) {
            if(i > startIndex && nums[i] == nums[i-1]){
                continue;
            }
            path.add(nums[i]);
            backTracking(nums,i + 1);
            path.removeLast();
        }
    }
}

全排列

一、思路

全排列的话[1,2]和[2,1]是不一样的排列,与组合不同,排列要求顺序,所以不需要使用startIndex,只需判断元素是否使用过,如果不判断同一条路径path上的结果是否使用过的话,会一直重复遍历同一个元素,例如:整数数组为[1,2,3,4],那么就会一直遍历1,有两种判断的办法:
一是used数组,二是path.contains(nums[i])

二、解题方法

used数组
在这里插入图片描述

  1. 递归函数的返回值以及参数:
    参数为nums整数数组,创建一个used布尔类型的数组,长度为nums数组的长度
private void permuteHelper(int[] nums)
  1. 回溯函数终止条件:
    到达叶子结点时,就把结果添加到结果集里,退出递归
if (path.size() == nums.length){
    result.add(new ArrayList<>(path));
    return;
}
  1. 单层搜索的过程:
    for循环从第一个元素开始遍历,used数组判断如果为true,则跳过这次循环,找数组里下一个元素,没有使用过的话,就把元素添加到path里,并让used数组为true,递归调用,回溯撤销元素,让used数组为false
for (int i = 0; i < nums.length; i++){
            if (used[i]){//使用过该元素
                continue;
            }
            used[i] = true;
            path.add(nums[i]);
            permuteHelper(nums);
            path.removeLast();
            used[i] = false;
        }

path.contains(nums[i]):
只要把for循环内的判断used数组改为path.contains(nums[i])即可,判断path有没有添加过该元素

if(path.contains(nums[i])){
    continue;
}

三、Code

used数组

class Solution {
    List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
    LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
    boolean[] used;
    public List<List<Integer>> permute(int[] nums) {
        if (nums.length == 0){
            return result;
        }
        used = new boolean[nums.length];
        permuteHelper(nums);
        return result;
    }

    private void permuteHelper(int[] nums){
        if (path.size() == nums.length){
            result.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++){
            if (used[i]){//使用过该元素
                continue;
            }
            used[i] = true;
            path.add(nums[i]);
            permuteHelper(nums);
            path.removeLast();
            used[i] = false;
        }
    }
}

path.contains(nums[i])

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

    public List<List<Integer>> permute(int[] nums) {
        if (nums.length == 0){
            return result;
        }
        backTracking(nums);
        return result;
    }

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

        for(int i = 0;i<nums.length;i++) {
            if(path.contains(nums[i])){
                continue;
            }
            path.add(nums[i]);
            backTracking(nums);
            path.removeLast();
        }
    }
}

全排列II

一、思路

此题比上面的 全排列I 只多了一个数组元素可重复,与组合总和II、子集II是相同的都要进行先排序再去重,用到used数组判断同一层是否使用过,使用过的话,直接跳过这次循环
在这里插入图片描述

二、解题方法

与全排列I使用used数组的回溯三部曲大致相同,仅仅多了先对数组进行排序,随后判断同一层是否使用过,这个与组合总和II,子集II的通过used数组判断去重是一样的,在此省略

三、Code

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

    public List<List<Integer>> permuteUnique(int[] nums) {
        if (nums.length == 0){
            return result;
        }
        used = new boolean[nums.length];
        Arrays.sort(nums);
        backTracking(nums);
        return result;
    }

    private void backTracking(int[] nums){
        if (path.size() == nums.length){
            result.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++){
            // 同一层用过直接跳过去重
            if(i>0 && nums[i] == nums[i-1] && used[i-1] == false){
                continue;
            }

            // 同一条结果路径上没有使用过
            if (used[i] == false){
                used[i] = true;
                path.add(nums[i]);
                backTracking(nums);
                path.removeLast();
                used[i] = false;
            }
            
        }
    }
}

总结

以上就是针对这道题的刷题笔记,通过这三篇博客,我们可以看到回溯算法在解决组合、排列等问题时的应用。其基本思想是通过递归和回溯来探索所有可能的解空间,并及时剪枝以避免重复计算,从而高效地求解问题。可以总结出如下回溯的套路:

去重:先对数组进行排序再判断同一层是否使用过相同元素if(i>0 && nums[i] == nums[i-1] && used[i-1] == false){ continue; }

排列和组合都是在叶子结点收集结果集,而子集则是在树的每一个结点都要进行收集结果集

希望本文对你理解和解决组合总和问题有所帮助!

标签:used,nums,46,47,力扣,int,result,new,path
From: https://blog.csdn.net/Huahua_1223/article/details/137287556

相关文章

  • ABC347G 题题解
    还算是比较经典了。首先我们注意到一个性质:\(1+3+\cdots+n=n^2\)。所以我们可以把平方拆开。然后容易证明\(a_{i,j}\)填\(1\)一定比填\(0\)不劣。我们可以把\(a_{i,j}\)拆成\(4\)个点,然后我们想到了最小割。构造网络:\(a_{i,j,x}\leftarrowa_{i,......
  • 1946C - Tree Cutting
    CF难度:1600从题意中看是一道比较经典的树形DP问题,题目要求的是把一棵树切k次,求被分割的树中最小的节点x的最大值是多少,因此可以采用二分写法,对于每一个树中,满足mid的节点个数是否等于k个。题目链接:TreeCutting对于每个二分,都用一遍dfs遍历树,因此check函数可以这么写:boolc......
  • P4678
    dp#概率期望\(dp_{i,x,y,z}\)表示在不使用\(i\)的情况下,达到有\(x\)个石头,\(y\)个剪刀,\(z\)个布的情况的概率转移可以做背包,每次加入一个对于每个可能得状态,统计所有可能的下一步的每一个方案的期望,然后取最大值//Author:xiaruize#ifndefONLINE_JUDGEboolstart_......
  • CMSE11475金融机器学习
    金融机器学习(CMSE11475)项目说明该项目旨在实践使用最先进的机器学习模型来分析财务数据和解决财务问题。单个项目:该项目是单独的项目。不需要任何组。学生应根据数据选择自己的主题独自完成自己的研究问题。在学习中相互合作和讨论鼓励过程,但项目应由学生自己完成,而不是分组课业。......
  • 每日一题 --- 找出字符串中第一个匹配项的下标[力扣][Go]
    找出字符串中第一个匹配项的下标题目:28.找出字符串中第一个匹配项的下标给你两个字符串haystack和needle,请你在haystack字符串中找出needle字符串的第一个匹配项的下标(下标从0开始)。如果needle不是haystack的一部分,则返回-1。示例1:输入:haystack="sa......
  • Offer必备算法20_队列_宽搜bfs_四道力扣题详解(由易到难)
    目录①力扣429.N叉树的层序遍历解析代码②力扣103.二叉树的锯齿形层序遍历解析代码③力扣662.二叉树最大宽度解析代码④力扣515.在每个树行中找最大值解析代码本篇完。①力扣429.N叉树的层序遍历429.N叉树的层序遍历难度中等给定一个N叉树,返回其节......
  • ABC347 C~D~?(更新中)
    Portal:https://atcoder.jp/contests/abc347/tasksABC347只过了\(A,B\),再创新低,。。。遂来补题C-IdealHolidays题意简述输入\(n,a,b,d_1,d_2,…,d_n\),表示在Atcoder国每周分为\(a\)天休息日和\(b\)天工作日,现在有\(n\)个事件,第\(i\)个事件落在第\(d_i\)日。我忘了今天是这......
  • AtCoder Beginner Contest 346 G
    #G-Alone(atcoder.jp)ABC346这一场来说相对比较简单,F是一个细节比较多的二分,G也算是一个比较板子的题。简单说一下G题的思路。其实比较容易想到用两个数组维护第i个数\(a_i\)在第i位之前出现的位置,以及第i个数在第i位之后出现的位置。那么当前位的能够满足的......
  • 2024.2.13力扣每日一题——二叉树的垂序遍历
    2024.2.13题目来源我的题解方法一TreeMap+深度优先遍历方法二官方题解(自定义排序)数组实现欢迎讨论(做题中遇到的一个问题)题目来源力扣每日一题;题序:987我的题解方法一TreeMap+深度优先遍历在递归形式的前、中、后序遍历中任选一种进行遍历,并在遍历过程中记......
  • 2024.2.16力扣每日一题——二叉树的锯齿形层序遍历
    2024.2.16题目来源我的题解方法一双端队列+标志题目来源力扣每日一题;题序:103我的题解方法一双端队列+标志层序遍历利用双端队列和标志,判断当前应该往那个方向遍历注意:在逆向遍历时,加入后续节点到队列中的顺序需要改变时间复杂度:O(N),其中N为二叉树的......