LeetCode 47. Permutations II

Given a collection of numbers that might contain duplicates, return all possible unique permutations.

Example:

Input: [1,1,2]
Output:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
解析:
此题是上一个题目LeetCode 46. Permutations的后续,只不过上一个题目给出的集合中没有重复项,当前题目中有重复数字,并且要求返回的结果不重复。
因此在进行dfs递归时需要判断避免重复的生成,对候选集合进行排序,对每一个元素判断当前元素与前一个元素是否相等,如果相等,前面的数必须已经使用了,即对应的used中的值为1,当前的数字才能使用,否则需要跳过,这样就不会产生重复排列了。

 

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> ans;
        int num_lens;
        num_lens = nums.size();
        sort(nums.begin(), nums.end());
        if(nums.empty())
            return result;
        vector<bool> used(num_lens, false);
        dfs(nums, used, result, ans);
        return result;       
    }
    void dfs(vector<int> &nums, vector<bool> &used, vector<vector<int>> &result, vector<int> &ans)
    {
        if(ans.size() == nums.size())
            result.push_back(ans);
        else if(ans.size() > nums.size())
            return;
        else
        {
            for(int i=0; i < nums.size(); i++)
            {
                if(!used[i])
                {
                    if(i>0 && nums[i-1]==nums[i] && !used[i-1])
                        continue;
                    used[i] = true;
                    ans.push_back(nums[i]);
                    dfs(nums, used, result, ans);
                    ans.pop_back();
                    used[i] = false;
                }
            }
        }
    }
    
};

 



比较有意思的是:避免重复的这段代码
if(i>0 && nums[i-1]==nums[i] && !used[i-1]) continue; 和
if(i>0 && nums[i-1]==nums[i] && used[i-1]) continue;
竟然运行都可以通过,但是前一个时间更短,也是更推荐的。


由此可看,if(i>0 && nums[i-1]==nums[i] && !used[i-1])这种写法开始更节省时间的。具体分析一下:
我们以数组[1,2,2′]为例。
a.对于if(i>0 && nums[i-1]==nums[i] && used[i-1]) continue;的情况:
level 1,加入1,此时结果数组为[1],
递归进入level2,从i=0开始遍历:
i=0时,nums[0]=1,此时used为True;
i=1时,nums[1]=2,used为False,加入,此时结果数组为[1,2],
进入level3:
i=0时used为True;
i=1时used为True;
i=2时,nums[2]==nums[1]并且used[1]=true,所以此种情况不符合题意,跳过。
回溯到level2,之前遍历下标为1,现在继续遍历下标2,结果集为[1,2′]。
进入level3:
i=0时,used[0]=true;
i=1时,used[1]=fase;加入,得到结果为[1,2′,2]
对于结果集中第一个元素为2时,我们分析不符合题意的情况。
当结果集为[2,1]时,进入level3
i=0时,used[0]=true;i=1时,used[1]=true。i=2时,nums[2]==nums[1]并且used[1]==true,此时continue跳过。
不符合题意的两种情况中,在第level3进行判断时,前面的递归已经走到level2了,并且level2已经遍历完成,最后一步才发现不合适。
b.对于if(i>0 && nums[i-1]==nums[i] && !used[i-1]) continue;情况
我们只分析不符合题意的情况,
在level1级别,前面的1,2已经遍历完成,遍历到i=2时,结果集为空,此时判断是否要加入2’到结果集中,根据条件nums[2]==nums[1]&&used[1]==false,此种情况不满足,跳过,所以以2’开头的都已经跳过,后续不再进行遍历,这样就减少了遍历的次数,时间也会更短。

这里详细介绍一下剪枝逻辑中的!used[i-1]的判断逻辑。

为了方便研究,依然把相同的元素用上标’以示区别。

假设输入为nums = [1,2,2′],标准的全排列算法会得出如下答案:

[
    [1,2,2′],[1,2′,2],
    [2,1,2′],[2,2′,1],
    [2′,1,2],[2′,2,1]
]

显然,这个结果存在重复,比如[1,2,2′]和[1,2′,2]应该只被算作同一个排列,但被算作了两个不同的排列。

所以现在的关键在于,如何设计剪枝逻辑,把这种重复去除掉?

答案是,保证相同元素在排列中的相对位置保持不变。

比如说nums = [1,2,2′]这个例子,我保持排列中2一直在2’前面。

这样的话,你从上面 6 个排列中只能挑出 3 个排列符合这个条件:

[ [1,2,2′],[2,1,2′],[2,2′,1] ]

这也就是正确答案。

进一步,如果nums = [1,2,2′,2”],我只要保证重复元素2的相对位置固定,比如说2 -> 2′ -> 2”,也可以得到无重复的全排列结果。

仔细思考,应该很容易明白其中的原理:

标准全排列算法之所以出现重复,是因为把相同元素形成的排列序列视为不同的序列,但实际上它们应该是相同的;而如果固定相同元素形成的序列顺序,当然就避免了重复

那么反映到代码上,你注意看这个剪枝逻辑:

// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
    // 如果前面的相邻相等元素没有用过,则跳过
    continue;
}
// 选择 nums[i]

当出现重复元素时,比如输入nums = [1,2,2′,2”],2’只有在2已经被使用的情况下才会被选择,2”只有在2’已经被使用的情况下才会被选择,这就保证了相同元素在排列中的相对位置保证固定。

参考:https://mp.weixin.qq.com/s/nrTpZ9b9RvfNsaEkJoHMvg

Add a Comment

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据