DFS:带重复项的全排列,程序运行全流程解析

1. 问题描述

给出一组可能包含重复项的数字,返回该组数字的所有排列,结果以字典序 升序 排列。

示例:

输入:[1,1,2]

返回值:[[1,1,2],[1,2,1],[2,1,1]]

2. 核心逻辑

在处理 [1, 1, 2] 时,由于含有两个 1,如果套用最基础的 DFS 模板,就会产生大量冗余的重复排列。

例如:

  • 当我们以第一个 1 为开头,可以得到 [1, 1, 2][1, 2, 1]
  • 当我们以第二个 1 为开头,又会得到 [1, 1, 2][1, 2, 1]

这样得到的结果就含有重复项。

要想去除掉重复项,需要按照下面的逻辑进行处理:

  • 首先将数组 升序 排列,让相同的数字放在一起。
  • 然后,在递归树的同一层,如果 当前的数字和上一个数字相同且上一个数字已经尝试过了,那么当前这个数字就不应该再作为开头去尝试。

3. 完整C++代码实现

cpp 复制代码
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param num int整型vector 
     * @return int整型vector<vector<>>
     */
    vector<vector<int>> res;
    bool used[8] = {false};
    vector<int> path;

    void dfs(vector<int> &num)
    {
        if(path.size() == num.size())
        {
            res.push_back(path);
            return;
        }

        for(int i=0; i<num.size(); i++)
        {
            if(used[i]) continue;

            //注意这里i>0很关键,我们肯定不会在第一次循环就进行剪枝
            if(i>0 && num[i] == num[i-1] && !used[i-1]) continue;

            used[i] = true;
            path.push_back(num[i]);

            dfs(num);

            path.pop_back();
            used[i] = false;
        }
    }

    vector<vector<int> > permuteUnique(vector<int>& num) {
        //排序是剪枝的前提
        sort(num.begin(),num.end());
        dfs(num);
        return res;
    }
};

4. 程序运行全过程

假设输入数组已排序为 nums = [1, 1, 2],我们来分析一下 DFS 的完整流程,大家可以对照上面的代码看下面的流程分析:

从根节点出发:

  1. 选择 nums[0] ,此时 used = [T, F, F]push_back 之后 path = [1]
  2. 进入第二层递归:
    • 由于 used[0]true,所以会直接跳过第一轮 for 循环,选择第二轮 for 循环的 nums[1] ,此时 used = [T, T, F]push_back 之后 path = [1, 1]
    • 然后进入第三层递归:
      • 前两个 used 都为 true ,选择 nums[2],此时 used = [T, T, T]push_back 之后 path = [1, 1, 2]这是第一个结果
      • 在尝试进行第四次递归时,此时 已经满足path.size() == num.size(),于是进行 回溯
      • 回到第三层递归,执行 pop_back 并把 used[2] 置位 false,当前 used = [T, T, F]path = [1, 1]
      • 此时,for 循环达到边界,跳出循环,本次 dfs 调用结束,进行回溯。
    • 回溯到第二层,执行 pop_back 并把 used[1] 置位 false,当前 used = [T, F, F]path = [1]
    • 然后进行第二层的下一轮 for 循环,此时选择 nums[2]path = [1, 2]
      • 然后再进入第三次递归,选择 nums[1],此时,path = [1, 2, 1]
      • 得到了第二个结果 [1, 2, 1],然后进行回溯。
      • 此时,回溯到第二层之后,第二层递归的 for 循环也结束了,继续回溯。

程序又回到了根节点:

  • 撤销了 num[0],此时 used = [F, F, F]path = [],进入根节点的第二轮 for 循环。
  • 准备选择第二个 1 时,触发判断条件 nums[1] == nums[0]used[0] == false,这意味着 nums[0] 刚刚作为开头已经完整试过了所有可能性,并退回了。
  • 这时执行剪枝 ,用 continue 直接跳过第二个 1 。

收尾:

  • 循环进入 i = 2,选择 2 作为开头,此时 path = [2]
  • 后续逻辑同上,最终得到 [2, 1, 1]
相关推荐
数据牧羊人的成长笔记42 分钟前
逻辑回归与Softmax回归
算法·回归·逻辑回归
郑州光合科技余经理1 小时前
同城O2O海外版二次开发实战:从支付网关到配送算法
开发语言·前端·后端·算法·架构·uni-app·php
张健11564096482 小时前
使用信号量限制并发数量
开发语言·c++
jc06202 小时前
6.1云原生之Docker
c++·docker·云原生
d111111111d4 小时前
STM32-UART封装问题解析
笔记·stm32·单片机·嵌入式硬件·学习·算法
叶子野格5 小时前
《C语言学习:指针》12
c语言·开发语言·c++·学习·visual studio
Jiangxl~5 小时前
IP数据云如何为不同行业提供精准IP查询与风险防控解决方案?
网络·网络协议·tcp/ip·算法·ai·ip·安全架构
Fuyo_11195 小时前
C++ 内存管理
c++·笔记
李伟_Li慢慢6 小时前
wolfram详解山峦算法
前端·算法
counting money6 小时前
prim算法最小生成树(java)
算法