一、题目
二、求解思路及代码实现
回溯算法思路:
这道题目与之前讨论的全排列问题类似,但有一个关键的区别:本题中数组包含重复的数字,而之前的则没有重复数字。由于存在重复数字,这会导致生成重复的排列组合。因此,本题的关键在于如何有效地过滤掉这些重复的组合。如果不进行过滤,生成的排列中将会包含大量重复项。为了更直观地展示这一点,我们可以通过一个图示来演示示例一的情况,其中为了区分第一个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])