轮换分解得 m=n-环数;最短方案每步分裂一个环,逆操作为 Dénes/Cayley 合并计数,答案 m!·∏ c^(c-2)/(c-1)!(模 1e9+9)。
OJ: roj
题目 ID: 3093
难度:省选/NOI-
标签:组合数学数学排列逆元python
创建: 2026-10-01 16:27
更新: 2026-10-07 13:50
形式化题目
给定一个
先看样例
| 位置 |
1 | 2 | 3 |
|---|---|---|---|
| 2 | 3 | 1 | |
flowchart LR A((1)) -- "p₁=2" --> B((2)) B -- "p₂=3" --> C((3)) C -- "p₃=1" --> A
(沿箭头看:位置 1 的值 2 应去位置 2,位置 2 的值 3 应去位置 3,位置 3 的值 1 应去位置 1,
三者构成一个 3-环。)轮换个数
正解
思路
第一步:最少交换次数
把排列分解成轮换(不动点也算长度为 1 的轮换),设轮换个数为
- 若
在同一个轮换里:这个环被分裂成两个环, 增加 ; - 若
在不同轮换里:两个环合并成一个, 减少 。
flowchart LR
subgraph S["同一环:交换 (1,3) → 分裂"]
direction LR
a1((1)) --- a2((2)) --- a3((3))
end
subgraph T["分裂后"]
direction LR
b1((1)) --- b2((2))
b3((3))
end
S -- "+1 个环" --> T
图中 3-环
第二步:
关键约束:合并步不允许出现。若方案中出现一次「合并」(
- 长度为
的轮换在整个方案中恰被分裂 次; - 不同轮换的操作互不干扰,操作集合可以按组划分。
第三步:只数每一组的「分裂序列」。
把整个方案逆过来看:把
固定
个元素,把它们用 个对换的乘积表出某一个 -轮换的有序方式数恰为 。
所以每组贡献
种排法。合在一起,把每组的
用三个样例核对一下闭式:
| 排列 | 轮换长度 | 计算 | 答案 | |
|---|---|---|---|---|
| 2 | 3 | |||
| 2 | 2 | |||
| 0 | 1 |
与题面输出完全一致。实现上只需:读入后
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 16:27
# update_at: 2026-10-01 16:27
import sys
from math import factorial
MOD = 10**9 + 9
def cycle_lengths(p: list[int]) -> list[int]:
"""排列每个轮换的长度(不含长度 1 的不动点,它们不参与任何交换)。"""
n = len(p)
seen = [False] * n
return [length
for start in range(n)
if not seen[start]
for length in [walk_cycle(start, p, seen)]]
def walk_cycle(start: int, p: list[int], seen: list[bool]) -> int:
"""从 start 沿置换走一圈,标访问并返回这一圈的长度。"""
length = 0
j = start
while not seen[j]:
seen[j] = True
j = p[j] - 1 # p 存的是 1-based 的值,走的是「值 → 下标」这一步
length += 1
return length
def count_min_swaps(n: int, p: list[int]) -> int:
"""最少 m 次交换把 p 排好序的方案数。
m = n - 轮换个数;每次交换使轮换个数恰好 +1(分裂),所以最短方案中
每次交换都分裂一个轮换。逆着操作看是把 c 个元素合并成 c-轮换,由
Dénes 定理(Cayley 公式)恰有 c^(c-2) 种合并方式;再乘上
m 次操作的自由穿插 m!,除掉每组内部相对顺序 (c-1)!。
"""
lengths = cycle_lengths(p)
m = n - len(lengths) # 最少交换次数
ans = factorial(m) % MOD
for c in lengths:
if c > 1:
ans = ans * pow(c, c - 2, MOD) % MOD # 该组的合并方式数
ans = ans * pow(factorial(c - 1), MOD - 2, MOD) % MOD # 除以 (c-1)! 取模
return ans
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n = next(data)
p = [next(data) for _ in range(n)]
out.append(str(count_min_swaps(n, p)))
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 18:35
* update_at: 2026-10-06 18:35
*/
#include <cstdio>
using namespace std;
typedef long long ll;
const ll MOD = 1000000009; // 题目要求的模数,注意不是常见的 1e9+7
const int MAXN = 100005; // n 的最大规模
int p[MAXN]; // p[i] 表示排列第 i 个位置上的数值(下标从 1 开始)
int vis[MAXN]; // vis[i] 标记位置 i 是否已经在轮换分解中被走过
ll fact[MAXN]; // fact[i] = i! mod MOD
// 快速幂:计算 base^exp mod MOD。
ll mod_pow(ll base, ll exp) {
ll res = 1 % MOD;
base %= MOD;
while (exp > 0) {
if (exp & 1) res = res * base % MOD;
base = base * base % MOD;
exp >>= 1;
}
return res;
}
// 预处理阶乘,供 m! 和 (c-1)! 使用。
void init_fact() {
fact[0] = 1;
for (int i = 1; i < MAXN; i++) {
fact[i] = fact[i - 1] * i % MOD;
}
}
void solve() {
int T;
scanf("%d", &T);
while (T--) {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &p[i]);
vis[i] = 0;
}
// 沿「位置 i -> 位置 p[i]」走,把排列分解成若干轮换。
// 设轮换个数为 k,则最少交换次数 m = n - k;
// 每个长度 c 的非平凡轮换贡献 c^(c-2) * ((c-1)!)^(-1)(Dénes 定理)。
int cycle_count = 0;
ll ans = 1;
for (int i = 1; i <= n; i++) {
if (vis[i]) continue;
int len = 0;
int j = i;
while (!vis[j]) {
vis[j] = 1;
j = p[j];
len++;
}
cycle_count++;
if (len > 1) {
ans = ans * mod_pow(len, len - 2) % MOD; // c^(c-2)
ans = ans * mod_pow(fact[len - 1], MOD - 2) % MOD; // 除以 (c-1)!
}
}
ll m = n - cycle_count; // 最少交换次数
ans = ans * fact[m] % MOD; // 再乘上 m 次操作的穿插方案数 m!
printf("%lld\n", ans);
}
}
int main() {
init_fact();
solve();
return 0;
}复杂度
- 时间复杂度:每组数据轮换分解
,每个非平凡轮换各一次 快速幂,合计 上界;读入用一次性 read().split()。 - 空间复杂度:
,排列数组和访问标记。
总结
这道题的骨架是「模型变换 + 组合计数」:先用轮换结构把「最少交换次数」变成
图示解析
从轮换视角完整回放样例
| 步骤 | 操作 | 排列 | 轮换个数 |
|---|---|---|---|
| 初始 | — | 1(一个 3-环) | |
| 第 1 步 | 交换位置 1、2 | 2(3-环分裂出 |
|
| 第 2 步 | 交换位置 2、3 | 3(全是不动点) |
每一步环数都