Link
Solve
对于一个排列(p_1,p_2,...,p_n),如果从每个(i)向(p_i)建一条边,显然可以得到一张图,这张图包括若干个环组成(包括自环)。最后的目标序列为(1,2,...,n),显然由(n)个自环组成。
这里我们有一个引理:
把一个长度为(n)的环变成(n)个自环,最少需要(n-1)次操作。
证明:
首先把长度为(2)的环变成(2)个自环,显然需要一种操作。
假设(∀k≤n-1),把长度不超过(k)的环变成(k)个自环最少需要(k-1)次操作。当(k=n)时,设该环为(v_1→v_2→v_3→...→v_n→v_1),任意交换(v_i,v_j(i<j))的出边后,我们得到(v_{i+1}→v_{i+2}→v_{i+3}→...→v_j→v_{i+1})和(v_{1}→v_{2}→v_{i}→...→v_{j+1}→v_{j+2}→...→v_n→v_1)两个环。
两者的长度分别是(j-i)和(n-(j-i))。把两者分别拆分成自环的最小交换次数为(j-i-1)和(n-(j-i)-1)两者相加是(n-2),加上刚才(v_i)和(v_j)的交换,总共需要(n-1)次交换。最后通过数学归纳法可知,原命题成立。
证毕
设(F_n)表示用最少步数把一个长度为(n)的环变成(n)个自环,总共有多少中操作方法。由上面的证明过程可知,把一个长度为(n)的环变成(n)个自环的过程中,可以把该环拆成长度为(x)和(y)的两个环,其中(x+y=n),设(T(x,y))表示有多少种交换方法可以把长度为(n)的环变成长度为(x)和(y)的两个环,易得:
另外,两者各自变为自环的方法数为(F_x)和(F_y),步数为(x-1)和(y-1)。
根据多重集的排列数·加法原理和乘法原理:
如果最初的排列(p_1,P-2,...,p_n)由长度为(l_1,l_2,...,l_k)的(k)个环组成,其中(l_1+l_2+...l_k=n),那么最终答案就是
因为(10^9+9)是一个质数,所以就可以用乘法逆元算上面的除法,整个算法的时间复杂度是(O(n^2))。事实上,我们可以递推出(F_n)的前几项找规律,数感好的同学应该可以找出规律,或者把前几项输入到(OEIS)网站中,我们都可以得到通项公式(F_n=n^{n-2}),从而可以用快速幂优化到(O(nlogn))。
代码不难,数学推理难。
Code
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int TT=1000000009,maxn=100005;
int N,a[maxn],T,vis[maxn],cnt;
typedef long long LL;
LL ans,Fc[maxn],F[maxn],size[maxn];
int read(){
int ret=0,f=1;char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-')f=-f;ch=getchar();}
while(ch>='0'&&ch<='9') ret=ret*10+ch-'0',ch=getchar();
return ret*f;
}
LL Pow(LL a,LL b){
LL w=a,s=1;
while(b){
if(b&1)s=w*s%TT;
w=w*w%TT;
b>>=1;
}
return s;
}
int main(){
freopen("P4778.in","r",stdin);
freopen("P4778.out","w",stdout);
T=read();
Fc[0]=1;for(int i=1;i<maxn;i++)Fc[i]=Fc[i-1]*i%TT;
F[1]=1;for(int i=2;i<maxn;i++)F[i]=Pow(i,i-2);
while(T--){
N=read();
for(int i=1;i<=N;i++)a[i]=read();
cnt=0;memset(vis,0,sizeof vis);
int now_x;
for(int i=1;i<=N;i++)if(!vis[i]){
now_x=i;int num=0;
while(!vis[now_x]){vis[now_x]=1;now_x=a[now_x];num++;};
size[++cnt]=num;
}
ans=Fc[N-cnt];
for(int i=1;i<=cnt;i++){
LL inv=Pow(Fc[size[i]-1],TT-2);
ans=ans*F[size[i]]%TT*inv%TT;
}
printf("%lld
",ans);
}
return 0;
}