【容斥原理】 Counting swaps
传送门
题意
给定一个排列\(p_{1},p_{2},\dots,p_{n}\),规定操作是选择两个数进行交换,设将排列变成\(1,2,/dots,n\)需要最少次数需要m次交换,求出有多少次不同的操作达到升序排列的目标,对答案模\(10^{9} + 9\)
数据范围
\(1\leq n\leq 10^{5}\)
题解
对于一个排列\(p_{1},p_{2},\dots,p_{n}\),每个\(i\) 向 \(p_{i}\)连一条边,那么可以得到\(n\)个点的无\(n\)条边的图,
并且图是由若干边组成。
引理:将一个长度为\(n\)的环边成\(n\)个自环,最少需要\(n-1\)次交换。
其中长度为n的环必然满足这样\(2,3,/\dots,n,1\) 的大小关系
引理的简单证明:
长度为\(2\)的环, 显然需要2次交换,
假设\(\forall k<n\)长度为\(k\)的环交换变成升序的最少交换都是\(k-1\)
长度为n的环\(v_{1} \rightarrow v_{2} \rightarrow \cdots \rightarrow v_{n} \rightarrow v_{1}\)
交换\(v_{i}\)和\(v_{j}\)\((i<j)\)的出边即\(v_{i+1}\)和\(v_{j+1}\)后,环被拆成了
这两个环,两个环的长度分别是\(j-i\)和\(n-(j-i)\),
两个环的最少交换次数是\((j-i-1)\)和\(n-(j-i)-1\) 加上刚才的一次交换即\(n-1\)
数学归纳法得证
用\(f_{n}\)表示将长度为\(n\)的环变成\(n\)自环的不同操作数量,在变成自环的过程中
可以将该环拆成长度为\(x\)和\(y\)的两个不同的环,\(x+y=n\),
设\(g(x,y)\)表示有多少种交换方法可以把长度为\(n\)的环拆分成长度为\(x,y\)的两个环
那么可以得到
\(x\)和\(y\)变成自环的不同的交换个数分别为\(f_{x}\),\(f_{y}\),交换次数分别是\(x-1\),\(y-1\),两个分别是独立的所以只需要计算交错排列的,
\(x\)和\(y\)内部的交换在\(f_{x}\)和\(f_{y}\)递归中\(g\)所统计所以只需要执行\(x\)和\(y\)的先后排列即可。
假设排列\(p_{1},p_{2},\dots,p_{n}\)中有长度为\(l_{1},l_{2},\dots,l_{k}\)的\(k\)个环组成,\(l_{1}+l_{2}+\dots+l_{k}=n\),那么最终的答案就是
\(10^{9} + 9\)是质数快速幂处理逆元即可,求\(f\)是\(O(n)\)的,复杂度是\(O(n \times k)\)
打表前10项得到
带入OEIS中可以得到通项公式为
这样可以将求\(f\)复杂度降到\(O(logn)\)
Code
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10,mod=1e9+9;
ll jc[N];
bool v[N];
ll p[N];
ll quick_pow(ll a,ll b){
ll res=1;
while(b){
if(b&1) res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
int main(){
jc[0]=1;
for(int i=1;i<=100000;i++){
jc[i]=jc[i-1]*i%mod;
}
int t;
cin>>t;
while(t--){
int n;
cin>>n;
for (int i = 1; i <= n; i++) scanf("%lld",&p[i]);
fill(v,v+n+1,0);
int cnt = 0;
ll ans = 1;
for (int i = 1; i <= n; i++) {
if (v[i]) continue;
int len = 1;//环的长度
v[i] = 1;
for (int j = p[i]; j != i; j = p[j]) v[j] = 1, len++;
cnt++;
ans = ans * (len == 1 ? 1 : quick_pow(len, len - 2)) % mod;
ans = ans * quick_pow(jc[len - 1], mod - 2) % mod;
}
ans = ans * jc[n - cnt] % mod;
printf("%lld\n",ans);
}
return 0;
}

浙公网安备 33010602011771号