全排列
从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列
公式:全排列数f(n)=n!(定义0!=1)
例如1,2,3的全排列为共有3!=6种,按字典序从小到大为:
1,2,3
1,3,2
2,1,3
2,3,1
3,1,2
3,2,1
DFS
DFS求全排列的思想是:把n个数建立成一个含有n个节点的无向图,那么我们对每一个节点跑一次深搜,就能得到一个由该起点开始的全排列,那么一个for循环就会得到所有的全排列
如上图,实际上是构建了一个全排列的森林,从上到下分别是第一层dfs循环,第二层dfs循环,第三层dfs循环。显然当我们递归到第四层时就需要return,并且每一层执行完dfs需要清空标记数组,这样才能继续递归。当然也可以像二叉树的DFS那样理解,只是我们实现的是线性数据结构的dfs
值得注意的是,对输入序列进行排序后,最后得到的全排列也会是按字典序依次输出
const int maxn=?;
int a[maxn],ans[maxn]; //存输入的数组
bool vis[maxn];
int n; //当前序列长度
int cnt; //记录全排列种数
void dfs(int x){ //x表示当前已得到序列的长度
if(x==n+1){
//如果第n+1次仍然进入该循环那么就表示之前的n次存储了一次全排列,输出返回
for(int i=1;i<=n;i++) printf("%d%c",ans[i],i==n?'\n':' ');
cnt++;
return;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
ans[x]=a[i]; //赋值到ans数组
vis[i]=1;
dfs(x+1);
vis[i]=0; //重要操作
}
}
}
参考过紫书之后,发现也可以不设置vis数组,而是统计每次ans数组里是否含有当前的a[i]即可
int a[maxn],ans[maxn];
int n,cnt;
void print_permutation(int cur){
if(cur==n+1){
for(int i=1;i<=n;i++) printf("%d%c",ans[i],i==n?'\n':' ');
cnt++; //统计排列数量
return;
}
for(int i=1;i<=n;i++){
int ok=1;
for(int j=1;j<=cur;j++)
if(ans[j]==a[i]) ok=0; //如果当前a[i]已经进入ans数组则不需生成下面的排列
if(ok){
ans[cur]=a[i];
print_permutation(cur+1);
}
}
}
生成可重集的全排列
通过观察我们发现上面生成的只能是不可重集的全排列,即各种数字不能相同,如果我们输入1 1 1的话不会得到期望的结果,实际上我们只需要一个即可,那我们可以这样改进:用map统计一下当前排列是否出现过即可,但是这样只能用dfs的方法,上面紫书的思路仍然没有输出
map<string,int> mp; //每次使用过后注意清空
int a[maxn],ans[maxn];
bool vis[maxn];
int n,cnt;
void dfs(int x){
if(x==n+1){
string s="";
for(int i=1;i<=n;i++) s+=ans[i]+'0';
if(!mp[s]){
mp[s]=1;
cout<<s<<endl;
cnt++;
}
return;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
ans[x]=a[i];
vis[i]=1;
dfs(x+1);
vis[i]=0;
}
}
}
STL内置函数
PS:既适用于不可重集也适用于可重集
next_permutation()
next_permutation(a,a+length)函数是按照字典序产生排列的,并且是从数组中当前的字典序开始依次增大直至到最大字典序。也就是说如果刚开始是升序,会一步一步排列直至降序
当排列已经是最大,返回假,否则真
int main()
{
int a[5]= {1,2,3,4};
do{
for(int i=0; i<4; i++)
cout<<a[i]<<" ";
cout<<endl;
}while(next_permutation(a,a+4));
return 0;
}
prev_permutation()
prev_permutation(a+length)函数生成给定序列的上一个较小的排列,顺序是从大到小。
当排列已经是最小,返回假,否则真