洛谷 P4778 Counting swaps 题解

原题链接

Solution

根据排列 \(p_1,p_2,\dots,p_n\) 构造图 \(G\),\(G\) 上存在 \(n\) 个顶点,分别标为 \(1,2,\dots,n\),对于每个 \(i\) 建立一条 \(p_i\) 到 \(i\) 的边。

注意到,\(G\) 的每个连通分支都是一个环图,设初始状态下有 \(k\) 个环图。

交换序列中两个元素,会使图发生变化。接下来,为表述方便,我们不区分元素与其对应的顶点。

交换一个连通分支内的元素,会使该连通分支分裂为两个小环图;交换两个连通分支间的元素,会使两个连通分支合并为一个大环图。

当交换结束、排列变为自然排列时,\(G\) 由 \(n\) 个自环构成。

当采用最优交换策略时,每次交换必使 \(G\) 的连通分支数增加 \(1\),即交换某个连通分支内的元素。显然,\(m=n-k\)。

特别地,对于单个 \(L\) 阶环图,其最小交换次数为 \(L-1\)。

设初始状态下 \(k\) 个环图的阶分别为 \(L_1,L_2,\dots,L_k\),\(L_1+L_2+\dots+L_k=n\)。

设将一个 \(L\) 阶环图通过 \(L-1\) 步有序的交换变为 \(L\) 个自环的方案数为 \(f[L]\),如 \(f[3]=3\)。

初始状态下的 \(k\) 个环图交换策略互相独立,利用多重组合数,可求出总方案数为

\[\dbinom{n-k}{L_1-1,L_2-1,\dots,L_k-1}\cdot \prod_{i=1}^{k}f[L_i]=\dfrac{(n-k)!}{\prod_{i=1}^{k}(L_i-1)!}\cdot\prod_{i=1}^{k}f[L_i] \]

接下来讨论如何计算 \(f\) 序列。

考虑所有顶点集为 \(V\) 的 \(L\) 阶环图,其中每个环图通过 \(L-1\) 步有序的交换变为 \(L\) 个自环的方案数均为 \(f[L]\),而 \(L\) 阶环图一共有 \((L-1)!\) 个,于是所有 \(L\) 阶环图的交换方案数总和等于 \((L-1)!\cdot f[L]\)。

我们根据一个环图构造一棵树:环图变为 \(L\) 个自环的每一步交换涉及一对元素,在一个新图中建立这些元素对的连边。由于交换共有 \(L-1\) 步,于是新图有 \(L-1\) 条边。因为这 \(L-1\) 步交换必将涉及 \(V\) 中所有元素,所以这个新图是连通的,进而这个新图是棵树。我们按照交换顺序给边建立编号:第一步交换构造的边标记为 \(1\),第二步交换构造的边标记为 \(2\)……

我们发现,每个环图的一种交换方案唯一对应着边带编号的树,而每个 \(V\) 上的边带编号的树也唯一对应着一种环图的交换方案(按照建树的过程逆向构造)。可以严谨证明,每个环图的一种交换方案与 \(V\) 上的边带编号的树间存在一一对应关系。于是,所有环图的交换方案数总和等于 \(V\) 上边带编号的树的数量。

由 Cayley 定理,不考虑边上编号时,树共有 \(L^{L-2}\) 棵。每棵树上再考虑边上编号的全排列,可知边带编号的树共有 \((L-1)!\cdot L^{L-2}\) 棵。由一一对应关系可知,所有 \(L\) 阶环图的交换方案数总和即 \((L-1)!\cdot L^{L-2}\)。

我们通过两种方法计算出了所有 \(L\) 阶环图的交换方案数总和,它们必然相等,于是有

\[(L-1)!\cdot f[L] = (L-1)!\cdot L^{L-2} \iff f[L]=L^{L-2} \]

我们找到了 \(f\) 序列的通项公式,至此问题全部解决。

Code

#include <bits/stdc++.h>
typedef long long i64;
using namespace std;
const i64 MOD = 1000000009ll;

i64 qpow(i64 x, i64 y) {
    i64 res = 1;
    while (y) {
        if (y & 1)
            res = res * x % MOD;
        x = x * x % MOD;
        y >>= 1;
    }
    return res;
}

i64 f(i64 l) {
    if (l == 1)
        return 1;
    return qpow(l, l - 2);
}

int count(vector<int>& p, int x) {
    int cnt = 0;
    while (p[x]) {
        ++cnt;
        int tmp = x;
        x = p[x];
        p[tmp] = 0;
    }
    return cnt;
}

i64 solve() {
    int n;
    cin >> n;

    vector<int> p(n + 1);
    for (int i = 1; i <= n; ++i)
        cin >> p[i];

    vector<int> L;
    for (int i = 1; i <= n; ++i) {
        int l = count(p, i);
        if (l) L.emplace_back(l);
    }

    int k = L.size();

    if (k == n) return 1;

    i64 t = 1;
    for (int i = 0; i < k; ++i)
        t = t * f(L[i]) % MOD;

    int M = n - k;
    for (int i = 0; i < k; ++i)
        M = max(--L[i], M);

    vector<i64> fac(M + 1);
    fac[0] = 1;
    for (int i = 1; i <= M; ++i)
        fac[i] = fac[i - 1] * i % MOD;

    vector<i64> inv_fac(M + 1);
    inv_fac[M] = qpow(fac[M], MOD - 2);
    for (int i = M - 1; i >= 0; --i)
        inv_fac[i] = inv_fac[i + 1] * (i + 1) % MOD;

    for (int i = 0; i < k; ++i)
        t = t * inv_fac[L[i]] % MOD;

    return t * fac[n - k] % MOD;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int t;
    cin >> t;
    while (t--)
        cout << solve() << '\n';
    return 0;
}
posted @ 2026-06-12 22:41  SHUddol  阅读(20)  评论(0)    收藏  举报