【2.2】回溯算法-解含有重复数字的全排列 II

一、题目

        给定一个可包含 重复数字 的序列nums,按任意顺序返回所有不重复的全排列。

二、求解思路及代码实现

回溯算法思路:

        这道题目与之前讨论的全排列问题类似,但有一个关键的区别:本题中数组包含重复的数字,而之前的则没有重复数字。由于存在重复数字,这会导致生成重复的排列组合。因此,本题的关键在于如何有效地过滤掉这些重复的组合。如果不进行过滤,生成的排列中将会包含大量重复项。为了更直观地展示这一点,我们可以通过一个图示来演示示例一的情况,其中为了区分第一个1和第二个1,我们分别用黑色和红色进行标记。

        为了过滤掉重复的数字并避免生成重复的排列组合,我们可以采用一种称为“剪枝”的策略。具体步骤如下:

        1. **排序数组**:首先对数组进行排序,这样相同的数字就会相邻排列。

        2. **剪枝条件**:在遍历数组生成排列的过程中,当我们遇到当前数字与前一个数字相同,并且前一个数字没有被使用时,我们就跳过当前分支,即进行剪枝。这样可以有效地避免生成重复的排列。

        通过这种方式,我们可以在生成排列的过程中直接过滤掉重复的组合,而不需要事后进行复杂的数组比较。下图展示了这一剪枝过程的示意图,帮助我们更直观地理解如何在生成排列时避免重复。

代码实现:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& tempList, vector<vector<int>>& res) {
    // 如果数组中的所有元素都使用完了,类似于到了叶子节点,
    // 我们直接把从根节点到当前叶子节点这条路径的元素加入
    // 到集合res中
    if (tempList.size() == nums.size()) {
        res.push_back(tempList);
        return;
    }
    // 遍历数组中的元素
    for (int i = 0; i < nums.size(); i++) {
        // 如果已经被使用过,则直接跳过
        if (used[i])
            continue;
        // 注意,这里要剪掉重复的组合
        // 如果当前元素和前一个一样,并且前一个没有被使用过,我们也跳过
        if (i > 0 && nums[i - 1] == nums[i] && !used[i - 1])
            continue;
        // 否则我们就使用当前元素,把他标记为已使用
        used[i] = true;
        // 把当前元素nums[i]添加到tempList中
        tempList.push_back(nums[i]);
        // 递归,类似于n叉树的遍历,继续往下走
        backtrack(nums, used, tempList, res);
        // 递归完之后会往回走,往回走的时候要撤销选择
        used[i] = false;
        tempList.pop_back();
    }
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
    // 先对数组进行排序,这样做目的是相同的值在数组中肯定是挨着的,
    // 方便过滤掉重复的结果
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    // boolean数组,used[i]表示元素nums[i]是否被访问过
    vector<bool> used(nums.size(), false);
    // 执行回溯算法
    vector<int> tempList;
    backtrack(nums, used, tempList, res);
    return res;
}

int main() {
    vector<int> nums = {1, 1, 2};
    vector<vector<int>> result = permuteUnique(nums);

    for (const auto& perm : result) {
        cout << "[";
        for (int i = 0; i < perm.size(); i++) {
            cout << perm[i];
            if (i < perm.size() - 1) cout << ", ";
        }
        cout << "]" << endl;
    }

    return 0;
}

        除了之前提到的剪枝方式,我们还可以采用另一种剪枝策略:在遍历数组生成排列的过程中,如果当前数字与数组中前一个数字相同,并且前一个数字已经被使用,我们就跳过当前分支,即进行剪枝。这种剪枝方式与之前的剪枝方式相反,但同样可以有效地避免生成重复的排列。下图展示了这种剪枝过程的示意图,帮助我们更直观地理解如何在生成排列时避免重复。

        这两种剪枝方式都是可以的, 一种是把整个大枝剪掉,一种是在每个大枝下面不停的剪小枝 。很明显第一 种剪枝效率更高一些,我们来看下代码

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

void backtrack(vector<int>& nums, vector<bool>& used, vector<int>& tempList, vector<vector<int>>& res) {
    // 如果数组中的所有元素都使用完了,类似于到了叶子节点,
    // 我们直接把从根节点到当前叶子节点这条路径的元素加入
    // 到集合res中
    if (tempList.size() == nums.size()) {
        res.push_back(tempList);
        return;
    }
    // 遍历数组中的元素
    for (int i = 0; i < nums.size(); i++) {
        // 如果已经被使用过,则直接跳过
        if (used[i])
            continue;
        // 注意,这里要剪掉重复的组合
        // 如果当前元素和前一个一样,并且前一个被使用了,我们也跳过
        if (i > 0 && nums[i - 1] == nums[i] && used[i - 1])
            continue;
        // 否则我们就使用当前元素,把他标记为已使用
        used[i] = true;
        // 把当前元素nums[i]添加到tempList中
        tempList.push_back(nums[i]);
        // 递归,类似于n叉树的遍历,继续往下走
        backtrack(nums, used, tempList, res);
        // 递归完之后会往回走,往回走的时候要撤销选择
        used[i] = false;
        tempList.pop_back();
    }
}

vector<vector<int>> permuteUnique(vector<int>& nums) {
    // 先对数组进行排序,这样做目的是相同的值在数组中肯定是挨着的,
    // 方便过滤掉重复的结果
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    // boolean数组,used[i]表示元素nums[i]是否被访问过
    vector<bool> used(nums.size(), false);
    // 执行回溯算法
    vector<int> tempList;
    backtrack(nums, used, tempList, res);
    return res;
}

int main() {
    vector<int> nums = {1, 1, 2};
    vector<vector<int>> result = permuteUnique(nums);

    for (const auto& perm : result) {
        cout << "[";
        for (int i = 0; i < perm.size(); i++) {
            cout << perm[i];
            if (i < perm.size() - 1) cout << ", ";
        }
        cout << "]" << endl;
    }

    return 0;
}
上面两种代码非常相似,唯一不同的就是下面这行,其他的都一样。
 if (i > 0 && nums[i - 1] == nums[i] && used[i - 1])
如果让我们选择的话,我们肯定会选择第一种方式,把整个大的枝给剪掉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城狮7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值