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]
相关推荐
AI棒棒牛2 小时前
SCI核心论文剖析:ICSD-YOLO:面向工业现场安全的实时智能检测算法
算法·yolo·目标检测·计算机视觉·目标跟踪·yolo26
郝学胜-神的一滴2 小时前
「栈与缩点的艺术」二叉树前序序列化合法性判定:从脑筋急转弯到工程实现
java·开发语言·数据结构·c++·python·算法
汀、人工智能2 小时前
[特殊字符] 第25课:合并两个有序链表
数据结构·算法·链表·数据库架构··合并两个有序链表
Hello.Reader2 小时前
双卡 A100 + Ollama 生产部署从安装、踩坑、调优到最终可上线方案
linux·人工智能·算法
计算机安禾2 小时前
【数据结构与算法】第30篇:哈希表(Hash Table)
数据结构·学习·算法·哈希算法·散列表·visual studio
AIminminHu2 小时前
OpenGL渲染与几何内核那点事-项目实践理论补充(三-1-(3):番外篇-当你的CAD打开“怪兽级”STL时:从内存爆炸到零拷贝的极致优化
c++·零拷贝·mmap·内存拷贝
水饺编程2 小时前
第4章,[标签 Win32] :SysMets3 程序讲解04,垂直滚屏重绘
c语言·c++·windows·visual studio
xiaoye-duck2 小时前
《算法题讲解指南:动态规划算法--子序列问题(附总结)》--32.最长的斐波那契子序列的长度,33.最长等差数列,34.等差数列划分II-子序列
c++·算法·动态规划