链接
https://www.acwing.com/problem/content/description/214/
题目
给定一个 1~n 的排列 p1,p2,…,pn,可进行若干次操作,每次选择两个整数 x,y,交换 px,py。
设把 p1,p2,…,pn 变成单调递增的排列 1,2,…,n 至少需要 m 次交换。
求有多少种操作方法可以只用 m 次交换达到上述目标。
因为结果可能很大,你只需要输出结果对 (10^9+9) 取模之后的值。
例如排列 (2,3,1) 至少需要2次交换才能变为 (1,2,3)。操作方法共有3种,分别是:
方法一:先交换数字(2,3),变成 (3,2,1),再交换数字(3,1),变成 (1,2,3)。
方法二:先交换数字(2,1),变成 (1,3,2),再交换数字(3,2),变成 (1,2,3)。
方法三:先交换数字(3,1),变成 (2,1,3),再交换数字(2,1),变成 (1,2,3)。
输入格式
第一行包含整数T,表示一共有T组测试用例。
每个测试用例前都会有一个空行。
每个测试用例包含两行,第一行包含整数n。
第二行包含n个整数,表示序列(p_1,p_2,…,p_n)。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
(1≤n≤10^5)
输入样例:
3
3
2 3 1
4
2 1 4 3
2
1 2
输出样例:
3
2
1
思路
思路把(a[j]=i)这个数连一条无向边到i,这样就会产生(cnt)个环,最终我们要将(cnt)个环拆成n分自环。
引理:首先把一个有(k)个点的简单环拆成(k)个自环需要(k-1)次交换
对于一个环,我们可以任选两个点交换变成两个小环。
设(f[i])表示在交换最少次数下,将长度为i的环拆成i个自环的方案数。
设(T(x,y))表示把长度为:(x+y)的环拆成两个长度为x,y的环的方案数。假设有x+y种方法,可以发现当x=y时,有一半的方法是对称的。即(x=y)时(T(x,y)=x),否则(T(x,y)=x+y)
x环和y环的操作互不干扰,我们将x环中的x-1步和y环上的y-1步排列,就是一个二重集的全排列。
这里算多重集合的排列数是因为多重集只算不同集合之间的排列关系,同一个集合当作相同;对于同一个集合内的排列关系通过f[x],f[y]再去算。
有cnt个环,每个环的长度为(l[i]) 最终的答案就是:
即使分析到了这一步,复杂度还是(O(n^2)).
通过打表(不是,题解,可知,f[i]=(i^{i-2}),通过快速幂:新的时间复杂度是(O(nlogn))
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=100010,mod=1e9+9;
int a[N],n,st[N],l[N],d;
int fac[N],f[N];
int qmi(int a,int b){
int res=1;
while(b){
if(b&1) res=(LL)res*a%mod;
a=(LL)a*a%mod;
b>>=1;
}
return res;
}
void dfs(int u){
d++;
st[u]=1;
if(!st[a[u]]) dfs(a[u]);
}
void init(){
fac[0]=1;
f[1]=1;
fac[1]=1;
for(int i=2;i<N;++i){
f[i]=qmi(i,i-2);
fac[i]=(LL)fac[i-1]*i%mod;
}
}
int main(){
int T;
init();
scanf("%d",&T);
while(T--){
memset(st,0,sizeof st);
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
int cnt=0;
for(int i=1;i<=n;++i){
if(!st[i]) {
d=0;
dfs(i);
l[++cnt]=d;
}
}
int ans=fac[n-cnt]%mod;
for(int i=1;i<=cnt;++i){
ans=(LL)ans*f[l[i]]%mod*(LL)qmi(fac[l[i]-1],mod-2)%mod;
}
cout<<ans<<endl;
}
return 0;
}