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

0 阅读3分钟

1. 问题描述

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

1. 题目描述.png

示例:

输入:[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++代码实现

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]