不会 Prüfer 序列
Prüfer 序列是大佬才能使用的“经典结论”,像我这样的渣滓根本不配。
n = m - 1
树计数,我们先随便钦定一个根,比如最后一个节点。
在树的情况下,根据定义,可知
我们考虑一种更适合普通人类的树计数方式:插入。
我们先把所有叶子(
每次插入一个节点,把这个节点的度数分成
最后一个节点不做插入,因为它是我们钦定的根,如果数据合法必然有最后“树顶”个数等于
其实代码更好懂……
void solve() {
// 已经提前将 a 数组排序,保证 a_i=1 在最前面
int ans = 1;
int deg = 0; // 树顶个数
for (int i = 1; i < n; ++ i) {
if (a[i] == 1) {
++ deg;
continue;
}
ans = (lnt) A(deg - 1, a[i] - 2) // 有序选择
% M * (i - 1)
% M * vca[a[i] - 1]
% M * ans % M
;
deg -= a[i] - 2; // 树顶会减少 a_i-2 个。
}
cout << ans << '\n';
}
n = m
基环树。
我们尝试从树的算法上推广。
容易发现,将环上的边全部断掉,并将原先在环上的点作为余下森林的每棵树的根,对于任意节点(包括原先在环上的点)均有
那么我们直接跑上面的算法,这次最后一个节点也一样加进去。然后只需要剩余的“树顶”连接成环即可。
基本一样的代码。
void solve() {
int ans = 1, deg = 0;
for (int i = 1; i <= n; ++ i) {
// 一模一样
// ...
}
cout << (lnt) ans * ica[deg - 1] % M * vca[2] % M << '\n';
}
n = m+1,a_i=1
那么整个图构成一个点双。
只可能是两个节点夹三条链。
那么:
-
- 中间三条链内任意填入
n-2 个数的方案数,减去有两条链都为空的方案数。 - 除以
3! ,消除三条链之间的顺序。
cout << (lnt) n * (n - 1) / 2 % M * vca[3] % M * (
(lnt) ica[n] * vca[2] % M + // 从 3 乘到 n
(lnt) (M - ica[n - 2]) * 3 // 选一条链,排列
) % M << '\n';
n=m+1 且两环有公共边。
人话:只有一个大点双。
同基环树的操作方式。如果将大点双内的所有边去掉,对于任意节点均有
那么就只需要把基环树最后一步的“连接成环”改为“连接成大点双”即可。
再一看,这个最后一步不就是上面
void solve() {
int ans = 1, deg = 0;
for (int i = 1; i <= n; ++ i) {
// 一模一样
// ...
}
return (lnt) deg * (deg - 1) / 2 % M * vca[3] % M * (
(lnt) ica[deg] * vca[2] % M +
(lnt) (M - ica[deg - 2]) * 3
) % M * ans % M;
}
n=m+1 且两环没公共边。
我们考虑直接跑基环树,那么现在还要造一个环上去。
此时这个基环上的点数为实际情况两环点数之和
那么我们直接随便选一条非基环上的边拆了,不就把度数补上了嘛。非基环上的边,即为所有非树顶节点的连向父亲的边,所以有
然后我们再从基环上取一段后缀分给新环。图要合法,所以环至少要
两个环要分别消除顺逆。同时两环地位等同,还要消除对面分裂出这边的情况。一共除以
void solve() {
int ans = 1, deg = 0;
for (int i = 1; i <= n; ++ i) {
// 一模一样
// ...
}
return (lnt) (deg - 3) * (n - deg)
% M * ica[deg - 1] // 这行和下面一个2,就是基环树
% M * vca[2]
% M * vca[2]
% M * vca[2]
% M * ans
% M
;
}
后记
模拟赛拿到
其实最后一个部分是观察小数据凑出来的,最后整理发现了很好的组合意义。