1 条题解

  • 0
    @ 2025-8-24 22:03:32

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar 基地A_I
    现实和理想之间,不变的是跋涉,暗淡与辉煌之间,不变的是开拓。

    搬运于2025-08-24 22:03:32,当前版本为作者最后更新于2020-01-21 14:43:38,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    博客园阅读地址

    Problem

    ACwing 题目地址

    洛谷 题目地址

    经典题,好题。另外感谢 Rose_max 大佬的博客让我搞懂了这题。


    Solution 1

    对于一个排列 p1,p2,...,pnp_1,p_2,...,p_n,每一个 pip_iii 连一条无向边,构成一张由若干个简单环组成的无向图。目标状态即 nn 个自环。

    引理:将一个大小为 kk 的简单环变成 kk 个自环,至少需要 k1k-1 次交换。

    利用数学归纳法,证明很简单,这里省略证明。

    由此我们可以得到一个推论:假设图中有 kk 个环,那么最小交换步数是 nkn-k


    下面我们来考虑计数。

    f[i]f[i] 表示一个大小为 ii 的环,在保证交换次数最少的情况下,有多少种方法将其变成目标状态。

    每一次交换可以把大小为 ii 的环拆成大小为 x,yx,y 的两个环,x+y=nx+y=n。设 T(x,y)T(x,y) 表示有多少种交换方法可以将一个大小为 ii 的环拆成两个大小分别为 x,yx,y 的环。可以发现:当 x=yx=y 时,有一半的方法是重复的,T(x,y)=xT(x,y)=x;否则 T(x,y)=x+yT(x,y)=x+y

    将一个大小为 ii 的环拆成大小为 x,yx,y 的两个环后,xx 环需要 x1x-1 次交换达到目标状态,将这 x1x-1 次操作记为 x1x-100,同理将 yy 环的 y1y-1 次操作记为 y1y-111由于 xx 环与 yy 环的操作互不干扰,两边的操作可以随意排列,因此这里就是一个多重集的全排列

    综上所述,可以得到 f[i]f[i] 的状态转移方程:

    $$f[i]=\sum_{x+y=i} f[x]*f[y]*T(x,y)*\frac{(i-2)!}{(x-1)!(y-1)!} $$

    (或许这个式子可以 FFTFFT 一下,如果你可以将其 FFTFFT,记得告诉我一下做法qwq)


    假设排列 p1,p2,...,pnp_1,p_2,...,p_n 中有 kk 个大小分别为 L1,L2,...,LkL_1,L_2,...,L_k 的环。因为这 kk 个环的操作互不干扰,可以记其中第 ii 个环的 Li1L_i-1 次操作为 Li1L_i-1ii总操作就是一个多重集的全排列。答案是:

    $$Ans=(\prod_{i=1}^k f[L_i])*(\frac{(n-k)!}{\prod_{i=1}^k (L_i-1)!}) $$

    mod109+9\mod 10^9+9 意义下,上述除法可以用费马小定理求得逆元解决。复杂度带一个 log\log

    时间复杂度:O(n2logn)O(n^2 \log n)

    f[ ]f[\ ] 数组和阶乘部分代码(预处理)

    void Init() {
    	fc[0] = 1;	//阶乘 factorial
    	for(int i=1;i<=1000;++i) fc[i] = fc[i-1] * i % mod;
    	f[1] = 1;
    	for(int i=2;i<=1000;++i) {
    		for(int j=1;j<=i/2;++j) {	// (x,y)和(y,x)只能算一次 
    			int inv = Pow(fc[i-j-1]*fc[j-1]%mod, mod-2);
    			f[i] = (f[i] + f[i-j]*f[j]%mod*T(i-j,j)%mod*fc[i-2]%mod*inv%mod) % mod;
    		}
    	}
    	for(int i=1;i<=10;++i) printf("%lld\n",f[i]);
    }
    

    Solution 2

    注意到上述算法并不能通过此题(n105n \leqslant 10^5)。瓶颈在于求 f[ ]f[\ ] 数组这里。于是我们打表出 f[ ]f[\ ] 数组的前 1010 项:

    1 1 3 16 125 1296 16807 262144 4782969 100000000

    将其输入进 OEISOEIS,或者是对乘方敏感的同学可以发现:f[n]=nn2f[n]=n^{n-2}。有了这个规律,我们便可以解决这个问题了。

    时间复杂度:O(nlogn)O(n \log n)

    Code

    Talk is cheap.Show me the code.

    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    inline int read() {
    	int x=0,f=1; char ch=getchar();
    	while(ch<'0' || ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    	while(ch>='0'&&ch<='9') { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); }
    	return x * f;
    }
    const int N = 1e5+7, mod = 1e9+9;
    int n,cnt,ans;
    int p[N],f[N],fc[N],L[N];
    bool vis[N];
    int Pow(int x,int y) {
    	int res = 1, base = x;
    	while(y) {
    		if(y&1) res = res*base%mod; base = base*base%mod; y >>= 1;
    	}
    	return res;
    }
    void Init2() {
    	fc[0] = 1;
    	for(int i=1;i<N;++i) fc[i] = fc[i-1] * i % mod;
    	f[1] = 1;
    	for(int i=2;i<N;++i) f[i] = Pow(i,i-2);
    }
    int Dfs(int u) {
    	vis[u] = 1;
    	if(vis[p[u]]) return 1;
    	return Dfs(p[u]) + 1;
    }
    void work() {
    	cnt = 0;
    	memset(vis, 0, sizeof(vis));
    	n = read();
    	for(int i=1;i<=n;++i) p[i] = read();
    	for(int i=1;i<=n;++i)
    		if(!vis[i]) L[++cnt] = Dfs(i);
    	ans = fc[n-cnt] % mod;
    	for(int i=1;i<=cnt;++i) {
    		int inv = Pow(fc[L[i]-1], mod-2);
    		ans = ans*f[L[i]]%mod*inv%mod;
    	}
    	printf("%lld\n",ans);
    }
    signed main()
    {
    	Init2();
    	int T = read();
    	while(T--) work();
    	return 0;
    }
    

    Summary

    模数看错自闭一中午,所以大家在任何地方都要细心呀!!

    这个题目让我学会两大经典模型:

    • 将排列 p1,p2,...,pnp_1,p_2,...,p_n 变成递增序列的最小交换次数。

    • 将排列 p1,p2,...,pnp_1,p_2,...,p_n 变成递增序列,保证最小交换次数下的方案数

    另外让我学会一些计数题的 技巧/思路/套路

    • 1

    信息

    ID
    3774
    时间
    1000ms
    内存
    32MiB
    难度
    6
    标签
    (无)
    递交数
    0
    已通过
    0
    上传者