Leetcode【回溯框架 递归】| 46. 全排列

这篇博客详细介绍了如何使用回溯算法解决LeetCode中的46. 全排列问题。通过动态规划递归和优化(结合回溯)的方式,阐述了如何找到给定序列的所有可能排列。博客内容包括决策树的回溯方法,强调了在递归过程中做选择和撤销选择的重要性,并给出了具体的解题框架。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
 输入: [1,2,3]
 输出:
 [
 [1,2,3],
 [1,3,2],
 [2,1,3],
 [2,3,1],
 [3,1,2],
 [3,2,1]
 ]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations

解题

动态规划递归

运用动态规划的思想,数组中n个元素的全排列,可以由n-1个元素的全排列递推而来。每次从数组中挑选出来一个元素放在第一个,加上剩下n-1个元素的全排列 (递归)
主题思想就是如何将原问题拆分为小规模的等价问题。

 dp[n] = ∑ i = 1 n \sum_{i=1}^n i=1nnums[i] & dp[n-1] (除nums[i]的n-1个元素的全排列)
 eg:dp[1,2,3] = [1,dp[2,3]] + [2,dp[1,3]] + [3,dp[1,2]]

代码参考这里

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        //判断特殊情况
        if(nums==null||nums.length==0){
            return res;
        }
        //遍历n个数,分别选择第i个数放在第一个,剩下的递归
        for(int i=0;i<nums.length;i++){
        	//去掉nums[i](这个元素被用来排列在第一位)的剩下n-1个元素用数组存储起来,以便之后递归求dp[n-1]
            int[] newnums = new int[nums.length-1];
            int m = 0;
            int n = 0;
            while(n<newnums.length){
                if(m!=i){
                    newnums[n] = nums[m];
                    n++;
                    m++;
                }else{
                    m++;
                }
            }
            //递归 求dp[n-1]用tmp存储起来,此时tmp里存储的就是已经递归求解出的剩余n-1个元素所有的全排列
            List<List<Integer>> tmp = permute(newnums);
            //temp为空时说明,此时只有一个元素,就是选出排列在第一位的nums[i],将其加入结果就行了。
            if(tmp.size()==0){
                List<Integer> list = new LinkedList<>();
                list.add(nums[i]);
                res.add(list);
            }else{
            	//将dp[n-1]即tmp中所有可能的全排列依次求出来,将nums[i]插到第一位,就是dp[n]中将nums[i]放在第一个的所有可能的全排列
                for(List<Integer> list:tmp){
                    LinkedList<Integer> list2 = new LinkedList<>(list);
                    list2.addFirst(nums[i]);
                    res.add(list2);
                }
            }
        }
        return res;
    }
}

在这里插入图片描述

动态规划优化(结合回溯)

交换+回溯

参考这里
每次选择nums[i]不用额外挑选出来,通过①nums[i]与第一位交换位置②递归③撤销交换(第一步)这三步实现nums[i]&dp[n-1]的原地操作。其实是结合了回溯的思想。

  1. 第一种方法即下面剪枝+回溯中介绍的方法是决策的时候通过判断选择的这个元素是否已经被选择过而决定这次是否选择这个元素,哪些为剩下的元素(用trace.contains()来判断)。
  2. 这个方法是每进行一次选择如第i次选择,将选择的元素放在原数组num的第i个位置上(交换),这样数组中i后面的元素都是还未选择过的剩下的元素(通过交换每次将剩下的元素都放在一起了),然后对后面的元素进行下一层选择/递归即可。比如nums = {1,2,3,4,5},当我们选择第一个元素为4的时候,我们将4放在第一个位置上,即与第一个位置上的元素进行交换nums变为{4,2,3,1,5},此时已选择序列为{4},待选择序列未{2,3,1,5},然后从第二个位置开始即待选择序列中递归进行第二个元素的选择。
class Solution {
    public List<List<Integer>> res;
    public List<Integer> trace;
    public List<List<Integer>> permute(int[] nums) {
        res = new ArrayList<>();
        backtrack(nums,0);
        return res;
    }
    public void backtrack(int[] nums, int begin){//begin为当前选择的第i个元素
        if(begin == nums.length){
            //就是最后一个元素第nums.length-1个元素已被选择完
            trace = new ArrayList<>();
            //所有选择完毕,在num中的位置也都交换完毕,可以直接全部添加
            for(int num:nums){
                trace.add(num);
            }
            res.add(trace);
            return;
        }else{
            //进行第i次选择,选择的范围为剩余的元素begin到nums.length
            for(int i = begin; i < nums.length; i++){
                //选择i,即将i放在begin的位置上,剩余的元素就是begin+1到nums.length
                //trace.add(nums[i]);//如果在选择的时候添加删减元素,会比交换到最后 一起添加元素 慢
                swap(nums,begin,i);
                //从剩余元素中继续下一次选择
                backtrack(nums,begin+1);
                //撤销选择,交换的位置再换回来,加入的元素也删除
                swap(nums,begin,i);
                //trace.remove(trace.size()-1);//在选择中添加元素,就不用最后一起添加了
            }
        }
    }
    public void swap(int[] nums, int i, int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

在这里插入图片描述

回溯 决策树

剪枝+回溯

参考这里

思想回溯思想,实质可以理解为树的深度优先搜索实现递归实现。
解决一个回溯问题,其实就是一个决策树的遍历过程,主要解决三个部分:

  1. 路径: 已做出的选择(在这道题中就是对nums[]进行排列从0到n-1依次选择的元素顺序)
  2. 选择列表: 当前可以做的全部选择(这道题中就是排列选择到第i个元素时还剩下哪些数可以选)
  3. 结束条件: 到达决策树底层,无法再做选择的条件。(这道题中就是选择到了最后一个元素此次排列完毕)

解法框架:

LinkeList<> res = new LinkedList<>();
public void recursive(路径,选择列表){
  if(满足结束条件){
    res.add(结果);
  }
  for(选择:选择列表){
    做出选择;
    recursive(路径,选择列表);//递归剩下的
    撤销选择;
  }
}

重点:在递归之前做选择,在递归之后撤销选择。在上一种解法中交换与撤销交换也是同理。

举例:以[1,2,3]为例,枚举出它的全排列的过程如下:(树的结点表示当前要做决策/选择,路径表示做出的选择,遍历的树所有的路径就是所求全排列结果)
全排列示例图

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        if(nums.length == 0) return res;
        recursive(nums,new LinkedList<Integer>());
        return res;
    }
    public void recursive(int[] nums, LinkedList<Integer> trace){
        //结束的条件,trace的深度等于所有元素的数量,即已遍历完所有元素
        if(trace.size() == nums.length){
            res.add(new LinkedList(trace));
            return;
        }
        //在选择列表中选出trace
        for(int i = 0; i < nums.length; i++){
            //剪枝,就是当前元素已经在组合中
            if(trace.contains(nums[i])) continue;
            //做出当前选择,然后递归 就做出了第一种路径的选择
            trace.add(nums[i]);
            recursive(nums,trace);
            //然后撤销当前选择,进行下一条路径的选择
            trace.removeLast();
        }  
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值