C++实现LeetCode(47.全排列之二)

[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]
]

这道题是之前那道 Permutations 的延伸,由于输入数组有可能出现重复数字,如果按照之前的算法运算,会有重复排列产生,我们要避免重复的产生,在递归函数中要判断前面一个数和当前的数是否相等,如果相等,且其对应的 visited 中的值为1,当前的数字才能使用(下文中会解释这样做的原因),否则需要跳过,这样就不会产生重复排列了,代码如下:

解法一:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res;
        vector<int> out, visited(nums.size(), 0);
        sort(nums.begin(), nums.end());
        permuteUniqueDFS(nums, 0, visited, out, res);
        return res;
    }
    void permuteUniqueDFS(vector<int>& nums, int level, vector<int>& visited, vector<int>& out, vector<vector<int>>& res) {
        if (level >= nums.size()) {res.push_back(out); return;}
        for (int i = 0; i < nums.size(); ++i) {
            if (visited[i] == 1) continue;
            if (i > 0 && nums[i] == nums[i - 1] && visited[i - 1] == 0) continue;
            visited[i] = 1;
            out.push_back(nums[i]);
            permuteUniqueDFS(nums, level + 1, visited, out, res);
            out.pop_back();
            visited[i] = 0;
        }
    }
};

在使用上面的方法的时候,一定要能弄清楚递归函数的 for 循环中两个 if 的剪枝的意思。在此之前,要弄清楚 level 的含义,这里用数组 out 来拼排列结果,level就是当前已经拼成的个数,其实就是 out 数组的长度。我们看到,for 循环的起始是从0开始的,而本题的解法二,三,四都是用了一个 start 变量,从而 for 循环都是从 start 开始,一定要分清楚 start 和本解法中的 level 的区别。由于递归的 for 都是从0开始,难免会重复遍历到数字,而全排列不能重复使用数字,意思是每个 nums 中的数字在全排列中只能使用一次(当然这并不妨碍 nums 中存在重复数字)。不能重复使用数字就靠 visited 数组来保证,这就是第一个 if 剪枝的意义所在。关键来看第二个 if 剪枝的意义,这里说当前数字和前一个数字相同,且前一个数字的 visited 值为0的时候,必须跳过。这里的前一个数 visited 值为0,并不代表前一个数字没有被处理过,也可能是递归结束后恢复状态时将 visited 值重置为0了,实际上就是这种情况,下面打印了一些中间过程的变量值,如下所示:

level = 0, i = 0 => out: {}
level = 1, i = 0 => out: {1 } skipped 1
level = 1, i = 1 => out: {1 }
level = 2, i = 0 => out: {1 2 } skipped 1
level = 2, i = 1 => out: {1 2 } skipped 1
level = 2, i = 2 => out: {1 2 }
level = 3 => saved  {1 2 2}
level = 3, i = 0 => out: {1 2 2 } skipped 1
level = 3, i = 1 => out: {1 2 2 } skipped 1
level = 3, i = 2 => out: {1 2 2 } skipped 1
level = 2, i = 2 => out: {1 2 2 } -> {1 2 } recovered
level = 1, i = 1 => out: {1 2 } -> {1 } recovered
level = 1, i = 2 => out: {1 } skipped 2
level = 0, i = 0 => out: {1 } -> {} recovered
level = 0, i = 1 => out: {}
level = 1, i = 0 => out: {2 }
level = 2, i = 0 => out: {2 1 } skipped 1
level = 2, i = 1 => out: {2 1 } skipped 1
level = 2, i = 2 => out: {2 1 }
level = 3 => saved  {1 2 2}
level = 3, i = 0 => out: {2 1 2 } skipped 1
level = 3, i = 1 => out: {2 1 2 } skipped 1
level = 3, i = 2 => out: {2 1 2 } skipped 1
level = 2, i = 2 => out: {2 1 2 } -> {2 1 } recovered
level = 1, i = 0 => out: {2 1 } -> {2 } recovered
level = 1, i = 1 => out: {2 } skipped 1
level = 1, i = 2 => out: {2 }
level = 2, i = 0 => out: {2 2 }
level = 3 => saved  {1 2 2}
level = 3, i = 0 => out: {2 2 1 } skipped 1
level = 3, i = 1 => out: {2 2 1 } skipped 1
level = 3, i = 2 => out: {2 2 1 } skipped 1
level = 2, i = 0 => out: {2 2 1 } -> {2 2 } recovered
level = 2, i = 1 => out: {2 2 } skipped 1
level = 2, i = 2 => out: {2 2 } skipped 1
level = 1, i = 2 => out: {2 2 } -> {2 } recovered
level = 0, i = 1 => out: {2 } -> {} recovered
level = 0, i = 2 => out: {} skipped 2

注意看这里面的 skipped 1 表示的是第一个 if 剪枝起作用的地方,skipped 2 表示的是第二个 if 剪枝起作用的地方。我们主要关心的是第二个 if 剪枝,看上方第一个蓝色标记的那行,再上面的红色一行表示在 level = 1, i = 1 时递归调用结束后,恢复到起始状态,那么此时 out 数组中只有一个1,后面的2已经被 pop_back() 了,当然对应的 visited 值也重置为0了,这种情况下需要剪枝,当然不能再一次把2往里加,因为这种情况在递归中已经加入到结果 res 中了,所以到了 level = 1, i = 2 的状态时,nums[i] == nums[i-1] && visited[i-1] == 0 的条件满足了,剪枝就起作用了,这种重复的情况就被剪掉了。

还有一种比较简便的方法,在 Permutations 的基础上稍加修改,用 TreeSet 来保存结果,利用其不会有重复项的特点,然后在递归函数中 swap 的地方,判断如果i和 start 不相同,但是 nums[i] 和 nums[start] 相同的情况下跳过,继续下一个循环,参见代码如下:

解法二:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        set<vector<int>> res;
        permute(nums, 0, res);
        return vector<vector<int>> (res.begin(), res.end());
    }
    void permute(vector<int>& nums, int start, set<vector<int>>& res) {
        if (start >= nums.size()) res.insert(nums);
        for (int i = start; i < nums.size(); ++i) {
            if (i != start && nums[i] == nums[start]) continue;
            swap(nums[i], nums[start]);
            permute(nums, start + 1, res);
            swap(nums[i], nums[start]);
        }
    }
};

对于上面的解法,你可能会有疑问,我们不是在 swap 操作之前已经做了剪枝了么,为什么还是会有重复出现,以至于还要用 TreeSet 来取出重复呢。总感觉使用 TreeSet 去重复有点耍赖,可能并没有探究到本题深层次的内容。这是很好的想法,首先尝试将上面的 TreeSet 还原为 vector,并且在主函数调用递归之前给 nums 排个序(代码参见评论区三楼),然后测试一个最简单的例子:[1, 2, 2],得到的结果为:

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

我们发现有重复项,那么剪枝究竟在做些什么,怎么还是没法防止重复项的产生!那个剪枝只是为了防止当 start = 1, i = 2 时,将两个2交换,这样可以防止 {1, 2, 2} 被加入两次。但是没法防止其他的重复情况,要闹清楚为啥,需要仔细分析一些中间过程,下面打印了一些中间过程的变量:

start = 0, i = 0 => {1 2 2}
start = 1, i = 1 => {1 2 2}
start = 2, i = 2 => {1 2 2}
start = 3 => saved  {1 2 2}
start = 1, i = 2 => {1 2 2} skipped
start = 0, i = 1 => {1 2 2} -> {2 1 2}
start = 1, i = 1 => {2 1 2}
start = 2, i = 2 => {2 1 2}
start = 3 => saved  {2 1 2}
start = 1, i = 2 => {2 1 2} -> {2 2 1}
start = 2, i = 2 => {2 2 1}
start = 3 => saved  {2 2 1}
start = 1, i = 2 => {2 2 1} -> {2 1 2} recovered
start = 0, i = 1 => {2 1 2} -> {1 2 2} recovered
start = 0, i = 2 => {1 2 2} -> {2 2 1}
start = 1, i = 1 => {2 2 1}
start = 2, i = 2 => {2 2 1}
start = 3 => saved  {2 2 1}
start = 1, i = 2 => {2 2 1} -> {2 1 2}
start = 2, i = 2 => {2 1 2}
start = 3 => saved  {2 1 2}
start = 1, i = 2 => {2 1 2} -> {2 2 1} recovered
start = 0, i = 2 => {2 2 1} -> {1 2 2} recovered

问题出在了递归调用之后的还原状态,参见上面的红色的两行,当 start = 0, i = 2 时,nums 已经还原到了 {1, 2, 2} 的状态,此时 nums[start] 不等于 nums[i],剪枝在这已经失效了,那么交换后的 {2, 2, 1} 还会被存到结果 res 中,而这个状态在之前就已经存过了一次。

注意到当 start = 0, i = 1 时,nums 交换之后变成了 {2, 1, 2},如果能保持这个状态,那么当 start = 0, i = 2 时,此时 nums[start] 就等于 nums[i] 了,剪枝操作就可以发挥作用了。怎么才能当递归结束后,不还原成为交换之前的状态的呢?答案就是不进行还原,这样还是能保存为之前交换后的状态。只是将最后一句 swap(nums[i], nums[start]) 删掉是不行的,因为递归函数的参数 nums 是加了&号,就表示引用了,那么之前调用递归函数之前的 nums 在递归函数中会被修改,可能还是无法得到我们想要的顺序,所以要把递归函数的 nums 参数的&号也同时去掉才行,参见代码如下:

解法三:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        permute(nums, 0, res);
        return res;
    }
    void permute(vector<int> nums, int start, vector<vector<int>>& res) {
        if (start >= nums.size()) res.push_back(nums);
        for (int i = start; i < nums.size(); ++i) {
            if (i != start && nums[i] == nums[start]) continue;
            swap(nums[i], nums[start]);
            permute(nums, start + 1, res);
        }
    }
};

好,再测试下 [1, 2, 2] 这个例子,并且把中间变量打印出来:

start = 0, i = 0 => {1 2 2}
start = 1, i = 1 => {1 2 2}
start = 2, i = 2 => {1 2 2}
start = 3 => saved  {1 2 2}
start = 1, i = 2 => {1 2 2} skipped
start = 0, i = 1 => {1 2 2} -> {2 1 2}
start = 1, i = 1 => {2 1 2}
start = 2, i = 2 => {2 1 2}
start = 3 => saved  {2 1 2}
start = 1, i = 2 => {2 1 2} -> {2 2 1}
start = 2, i = 2 => {2 2 1}
start = 3 => saved  {2 2 1}
start = 1, i = 2 => {2 2 1} recovered
start = 0, i = 1 => {2 1 2} recovered
start = 0, i = 2 => {2 1 2} skipped

明显发现短了许多,说明剪枝发挥了作用,看上面红色部分,当 start = 0, i = 1 时,递归函数调用完了之后,nums 数组保持了 {2, 1, 2} 的状态,那么到 start = 0, i = 2 的时候,nums[start] 就等于 nums[i] 了,剪枝操作就可以发挥作用了。

这时候你可能会想,调用完递归不恢复状态,感觉怪怪的,跟哥的递归模版不一样啊,容易搞混啊,而且一会加&号,一会不加的,这尼玛谁能分得清啊。别担心,I gotcha covered! 好,既然还是要恢复状态的话,就只能从剪枝入手了,原来那种 naive 的剪枝方法肯定无法使用,矛盾的焦点还是在于,当 start = 0, i = 2 时,nums 被还原成了 start = 0, i = 1 的交换前的状态 {1, 2, 2},这个状态已经被处理过了,再去处理一定会产生重复,怎么才知道这被处理过了呢,当前的 i = 2,需要往前去找是否有重复出现,由于数组已经排序过了,如果有重复,那么前面数一定和当前的相同,所以用一个 while 循环,往前找和 nums[i] 相同的数字,找到了就停下,当然如果小于 start 了也要停下,那么如果没有重复数字的话,j 一定是等于 start-1 的,那么如果不等于的话,就直接跳过就可以了,这样就可以去掉所有的重复啦,参见代码如下:

解法四:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        permute(nums, 0, res);
        return res;
    }
    void permute(vector<int>& nums, int start, vector<vector<int>>& res) {
        if (start >= nums.size()) res.push_back(nums);
        for (int i = start; i < nums.size(); ++i) {
            int j = i - 1;
            while (j >= start && nums[j] != nums[i]) --j;
            if (j != start - 1) continue;
            swap(nums[i], nums[start]);
            permute(nums, start + 1, res);
            swap(nums[i], nums[start]);
        }
    }
};

同样,我们再测试下 [1, 2, 2] 这个例子,并且把中间变量打印出来:

start = 0, i = 0 => {1 2 2} , j = -1
start = 1, i = 1 => {1 2 2} , j = 0
start = 2, i = 2 => {1 2 2} , j = 1
start = 3 => saved  {1 2 2}
start = 1, i = 2 => {1 2 2} skipped, j = 1
start = 0, i = 1 => {1 2 2} -> {2 1 2}, j = -1
start = 1, i = 1 => {2 1 2} , j = 0
start = 2, i = 2 => {2 1 2} , j = 1
start = 3 => saved  {2 1 2}
start = 1, i = 2 => {2 1 2} -> {2 2 1}, j = 0
start = 2, i = 2 => {2 2 1} , j = 1
start = 3 => saved  {2 2 1}
start = 1, i = 2 => {2 2 1} -> {2 1 2} recovered
start = 0, i = 1 => {2 1 2} -> {1 2 2} recovered
start = 0, i = 2 => {1 2 2} skipped, j = 1

到 start = 0, i = 2 的时候,j 此时等于1了,明显不是 start-1,说明有重复了,直接 skip 掉,这样剪枝操作就可以发挥作用了。

之前的 Permutations 中的解法三也可以用在这里,只不过需要借助 TreeSet 来去重复,博主还未想出其他不用集合的去重复的方法,哪位看官大神们知道的话,请一定要留言告知博主,参见代码如下:

解法五:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        if (nums.empty()) return vector<vector<int>>(1, vector<int>());
        set<vector<int>> res;
        int first = nums[0];
        nums.erase(nums.begin());
        vector<vector<int>> words = permuteUnique(nums);
        for (auto &a : words) {
            for (int i = 0; i <= a.size(); ++i) {
                a.insert(a.begin() + i, first);
                res.insert(a);
                a.erase(a.begin() + i);
            }
        }
        return vector<vector<int>> (res.begin(), res.end());
    }
};

之前的 Permutations 中的解法四博主没法成功修改使其可以通过这道题,即便是将结果 res 用 TreeSet 来去重复,还是不对。同样,哪位看官大神们知道的话,请一定要留言告知博主。后经过微信公众号上的热心网友 hahaboy 的提醒下,可以通过加上一个剪枝从而通过这道题,在最中间的 for 循环的最后,判断若 num 等于 t[i],直接 break 掉当前循环,否则会产生重复项,参见代码如下:

解法六:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res{{}};
        for (int num : nums) {
            for (int k = res.size(); k > 0; --k) {
                vector<int> t = res.front();
                res.erase(res.begin());
                for (int i = 0; i <= t.size(); ++i) {
                    vector<int> one = t;
                    one.insert(one.begin() + i, num);
                    res.push_back(one);
                    if (i < t.size() && num == t[i]) break;
                }
            }
        }
        return res;
    }
};

之前的 Permutations 中的解法五却可以原封不动的搬到这道题来,看来自带的 next_permutation() 函数就是叼啊,自带去重复功能,叼叼叼!参见代码如下:

解法七:

class Solution {
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        res.push_back(nums);
        while (next_permutation(nums.begin(), nums.end())) {
            res.push_back(nums);
        }
        return res;
    }
};

到此这篇关于C++实现LeetCode(47.全排列之二)的文章就介绍到这了,更多相关C++实现全排列之二内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++实现LeetCode(39.组合之和)

    [LeetCode] 39. Combination Sum 组合之和 Given a set of candidate numbers (candidates) (without duplicates) and a target number (target), find all unique combinations in candidates where the candidate numbers sums to target. The same repeated number may b

  • C++实现LeetCode(51.N皇后问题)

    [LeetCode] 51. N-Queens N皇后问题 The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens attack each other. Given an integer n, return all distinct solutions to the n-queens puzzle. Each solution contains a di

  • C++实现LeetCode(94.二叉树的中序遍历)

    [LeetCode] 94. Binary Tree Inorder Traversal 二叉树的中序遍历 Given a binary tree, return the inorder traversal of its nodes' values. Example: Input: [1,null,2,3] 1 \ 2 / 3 Output: [1,3,2] Follow up: Recursive solution is trivial, could you do it iteratively

  • C++实现LeetCode(90.子集合之二)

    [LeetCode] 90. Subsets II 子集合之二 Given a collection of integers that might contain duplicates, S, return all possible subsets. Note: Elements in a subset must be in non-descending order. The solution set must not contain duplicate subsets. For example

  • C++实现LeetCode(113.二叉树路径之和之二)

    [LeetCode] 113. Path Sum II 二叉树路径之和之二 Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given sum. For example: Given the below binary tree and sum = 22,  5 / \ 4   8 /      / \ 11  13  4 /  \         / \ 7  

  • C++实现LeetCode(Combinations 组合项)

    [LeetCode] Combinations 组合项 Given two integers n and k, return all possible combinations of k numbers out of 1 ... n. For example, If n = 4 and k = 2, a solution is: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 这道题让求1到n共n个数字里k个数的组合数的所有情况,还是要用深度优先搜索D

  • C++实现LeetCode(38.计数和读法)

    [LeetCode] 38. Count and Say 计数和读法 The count-and-say sequence is the sequence of integers with the first five terms as following: 1.     1 2.     11 3.     21 4.     1211 5.     111221 1 is read off as "one 1" or 11. 11 is read off as "two

  • C++实现LeetCode(47.全排列之二)

    [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] ] 这道题是之前那道 Permutations 的延伸,由于输入数组有可能出现重复数字,如果按照之前的

  • C++实现LeetCode(46.全排列)

    [LeetCode] 46. Permutations 全排列 Given a collection of distinct integers, return all possible permutations. Example: Input: [1,2,3] Output: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ] 这道题是求全排列问题,给的输入数组没有重复项,这跟之前的那道 Combinations 和类似,解法基本相同

  • C++实现LeetCode(倒置链表之二)

    [LeetCode] 92. Reverse Linked List II 倒置链表之二 Reverse a linked list from position m to n. Do it in one-pass. Note: 1 ≤ m ≤ n ≤ length of list. Example: Input: 1->2->3->4->5->NULL, m = 2, n = 4 Output: 1->4->3->2->5->NULL 很奇怪为何

  • C++实现LeetCode(74.搜索一个二维矩阵)

    [LeetCode] 74. Search a 2D Matrix 搜索一个二维矩阵 Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the following properties: Integers in each row are sorted from left to right. The first integer of each row is great

  • Java动态规划方式解决不同的二叉搜索树

    目录 一.题目描述 二.思路 三.代码 一.题目描述 给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数. 来源:https://leetcode.cn/problems/unique-binary-search-trees/ 二.思路 本题可以使用动态规划的方式解决,我们先来看一下大题思路.以 n = 3 为例,n = 3 时的不同的二叉搜索树数目,可以通过分别 以 1 为根节点,以 2 为根节点,以 3 为根节点

  • Tensorflow 卷积的梯度反向传播过程

    一. valid卷积的梯度 我们分两种不同的情况讨论valid卷积的梯度:第一种情况,在已知卷积核的情况下,对未知张量求导(即对张量中每一个变量求导):第二种情况,在已知张量的情况下,对未知卷积核求导(即对卷积核中每一个变量求导) 1.已知卷积核,对未知张量求导 我们用一个简单的例子理解valid卷积的梯度反向传播.假设有一个3x3的未知张量x,以及已知的2x2的卷积核K Tensorflow提供函数tf.nn.conv2d_backprop_input实现了valid卷积中对未知变量的求导,以

  • nacos gateway动态路由实战

    目录 nacos gateway动态路由 一.引入本次测试需要的pom依赖 二.配置文件设置(*配置文件不生效的把名字改为bootstrap.yml) 三.动态路由实现 四.nacos配置中心设置 五.测试 gateway网关相关配置 在启动类中添加注解@EeableDiscoveryClient 配置nacos注册中心地址 bootstrap.properties 填写配置中心地址 nacos里创建命名空间gateway 在主类中屏蔽数据源 在项目里创建application.yml naco

  • C语言三种方法解决轮转数组问题

    目录 题目 1.题目描述 2.要求 3.原题链接 二.相关知识点 三.解决思路 旋转法 直接法 空间换取时间 题目 1.题目描述 给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数. 示例 1: 输入: nums = [1,2,3,4,5,6,7], k = 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,1,2,3,4] 2.要求 进阶

  • Java跳跃游戏实例真题解决思路详解

    目录 变式题—跳跃游戏 I 一.题目描述 二.思路 三.代码 变式题—跳跃游戏 II 一.题目描述 二.思路 三.代码 变式题—跳跃游戏 I 一.题目描述 给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 .数组中的每个元素代表你在该位置可以跳跃的最大长度.判断你是否能够到达最后一个下标. 来源:https://leetcode.cn/problems/jump-game/ 示例: 二.思路 本题可以使用贪心法解决,对每个能到达的位置(可覆盖到的位置),计算其每次能覆盖的最大长度,

  • C++实现LeetCode(768.可排序的最大块数之二)

    [LeetCode] 768.Max Chunks To Make Sorted II 可排序的最大块数之二 This question is the same as "Max Chunks to Make Sorted" except the integers of the given array are not necessarily distinct, the input array could be up to length 2000, and the elements cou

随机推荐