给定一个 1~n 的排列 ,可进行若干次操作,每次选择两个整数 x,y,交换 变成单调递增的排列 1,2,…,n 至少需要 m 次交换。求有多少种操作方法可以只用 m 次交换达到上述目标。因为结果可能很大,你只需要输出对 10^9+9 取模之后的值。1≤n≤10^5。
例如排列 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。
原理(算法):快速幂,乘法原理,乘法逆元,找规律。
我们令第i个点连一条有向边到,那么我们的目标其实就是得到n个自环。
设表示把长度为n的环分为长度为x,y的环有多少种交换方法。容易发现,对于每个点,可以找到2个点来作为分界边的另一个点(当x≠y时),因为一条边被选两遍所以另外,当时,那么分法会重复,所以
综上:
引理:把一个长度为n的环变成n个自环最少需要n-1次交换操作。
证明:显然,每一次操作,最多只能将一个环变成两个环,所以将一个长度为n的环变成n个自环,最少也需要n-1次交换操作。
设为用最少步数,使得长度为n的环变成n个自环的方案数,那么:
其实相当于任选处理长度为x,y的环的顺序的方案数。换句话说,如果处理长度为x的环为1,处理长度为y的环为0,那么上述表达式求的是1,0的(多重集的)排列数。那找这个规律,我们可以打出一个规模较小的表,发现.
如果在n个点中,有长度为的k个环,,那么
综上:我们只须初始化(快速幂),就可以用的时间解决每一组数据。总时间复杂度。(gcd的复杂度不会证)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+10;
const int mod=1e9+9;
int n,a[N],l[N];bool vis[N];
ll jc[N],jc_inv[N],f[N],T,ans;
ll power_mod(ll a,ll b,ll c)
{
ll ans=1%c;a%=c;
while(b>0)
{
if(b&1)ans=ans*a%c;
a=a*a%mod;b=b>>1;
}
return ans;
}
void dfs(int x)
{
if(vis[x]){l[l[0]]++;return;}
vis[x]=1;++l[0];dfs(a[x]);
}
#define g getchar()
template<class o>void qr(o &x)
{
char c=g;x=0;
while(!('0'<=c&&c<='9'))c=g;
while('0'<=c&&c<='9')x=x*10+c-'0',c=g;
}
void write(ll x)
{
if(x/10)write(x/10);
putchar(x%10+'0');
}
int main()
{
qr(T);
jc[0]=jc_inv[0]=1;//0的阶乘等于1
for(int i=1;i<=100000;i++)
{
jc[i]=jc[i-1]*i%mod;
jc_inv[i]=power_mod(jc[i],mod-2,mod);//求逆元
f[i]=power_mod(i,i-2,mod);
}
while(T--)
{
qr(n);
memset(vis,0,sizeof(vis));
memset(l,0,sizeof(l));
for(int i=1;i<=n;i++)qr(a[i]);
int cnt=0;//环的个数(包括自环)
for(int i=1;i<=n;i++)if(!vis[i])l[0]=0,++cnt,dfs(i);//找环的长度
ans=jc[n-cnt];
for(int i=1;i<=n;i++)if(l[i])
{
n-=l[i]*i;//去掉l[i]*i个点
ans=ans*power_mod(f[i]*jc_inv[i-1],l[i],mod)%mod;//乘法结合律,小心溢出
}
write(ans);puts("");
}
return 0;
}