题目描述:
输入一组数字(可能包含重复数字),输出其所有的排列方式。
样例
输入:[1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
分析:
本题与普通的全排列问题的不同之处在于可能存在重复数字,不能输出重复的序列,比如1123和1123,。解决问题的关键在于定序,紫书上子集生成那节增量构造法便讲到了这种定序的技巧。
1123,我们规定第二个1只能出现在第一个1之后便不会出现重复的枚举了。
我们需要先给nums排序,方便定序。dfs与常规的排列问题一样,需要有当前的位置u,状态压缩时表示的状态state,这里为了定序增加了一个start,如果没有重复数字,start便等于0,可以从第一个位置开始放置数字,否则就要从上一个重复的数字后面放置数字。
在分析具体的算法之前,先总结下相似的dfs题目。
用向量保存中间状态时,只需要问题的规模n和当前数字cur作为dfs参数,递归体内写选择该数和不选择该数的情况,当达到一定的递归深度,输出方案即可。
而不使用向量,直接状态压缩,用st存储状态信息,这里的st就相当于向量的作用,只是更加节省空间了。
这题与之前题的区别就是在所有子集中输出固定个数的序列,在上一题的算法中加上已选择数的个数k,并加以判断即可,其他参数的含义并未改变。
这题是经典的全排列问题,与上面两题不同的是,上面的题目涉及的是选与不选某数,而这题所有数都必须被选,只是被选择的先后有所不同。所以使用状态压缩时依旧可以用st表示中间状态,但是到达边界时,st二进制的各个位置必然都是1,所以还需要向量来存储枚举的先后,并输出最终结果。
不妨回忆下此题代码
void dfs(int d,int st){
if(d == n ){
for(int i = 0;i < t.size();i++){
cout<<t[i]+1<<" ";
}
cout<<endl;
return;
}
for(int i = 0;i < n;i++){
if(!(st >> i & 1)){
t.push_back(i);
dfs(d+1,st | 1 << i );
t.pop_back();
}
}
}
如同AcWing 47 二叉树中和为某一值的路径中所分析的那样,递归的执行过程是,先不断把未放进向量的数放进去,放满了就再退出部分数,继续放,而在dfs的末尾加上这一句pop_back()操作就很完美的完成了这一复杂的回溯过程。
下面正式分析本题。
与刚才讨论全排列问题不同的是,交互式问题可能多几个向量保存结果,并且要加上定序。
我们从向向量放第一个数开始,直到放满指定个数的数结束。比如放置12344,1,2,3,4依次放入,再放入4时只能从下一个位置开始放。而对于不重复的123,我们枚举的范围是从0到结尾的。这里不需要写回溯语句的原因是,向量的值不是像之前那样不断压入弹出,而是从题目给的向量中直接赋值。这里最后一行的递归语句state + (1<<i)和上题的st | 1 << i功能是一样的。
class Solution {
public:
vector<vector<int> > ans;
vector<int> path;
vector<vector<int>> permutation(vector<int>& nums) {
path.resize(nums.size());
sort(nums.begin(),nums.end());
dfs(nums,0,0,0);
return ans;
}
void dfs(vector<int> &nums,int u,int start,int state){
if(u == nums.size()){
ans.push_back(path);
return;
}
if(!u || nums[u] != nums[u-1]) start = 0;
for(int i = start;i < nums.size();i++){
if(!(state >> i & 1)){
path[i] = nums[u];
dfs(nums,u+1,i+1,state + (1 << i));
}
}
}
};