题解 Aizu 2993【Invariant Tree】/ NODSX2304C【樟】
https://vjudge.net/problem/Aizu-2993 不是,怎么这种东西都有原题?而且是这个 OJ 独家题?
prufer 领域大师要开始表演了!
广义 Cayley 定理:https://www.cnblogs.com/caijianhong/p/solution-CF1109D.html
题目描述
给出长度为 \(n\) 的排列 \(p_{1\sim n}\),你需要计数满足下面条件的 \(n\) 点带标号无根树的数量:
- 如果存在树边 \((i,j)\),也存在树边 \((p_i,p_j)\)。
答案对 \(998244353\) 取模。一个测试点中有多组数据。
对于 100% 的数据,满足 \(1\le T,\sum n\le 5\times 10^5\)。
特殊性质 A:\(p_i = i\)。
特殊性质 B:一定不存在 \(p_i = i\)。
特殊性质 C:如果存在 \(p_i=j\),则 \(p_j\neq i\)。
特殊性质 D:如果存在 \(p_i=j\),则 \(p_j = i\)。
solution(n<=2)
输出 \(1\)。别小看这种情况,真要特判。
solution(n<=8)
枚举 prufer 序列并判定。
solution(A)
Cayley 定理:\(n\) 个点的无向完全图的生成树个数为 \(n^{n-2}\)。
solution(BD)
即排列中只有长度为 \(2\) 的置换环。两个长度为 \(2\) 的置换环之间连边,会连两条边,有两种方案。可以将每个长度为 \(2\) 的置换环看成一个大点(下文称之为:碳)。最终连出来的树的结构必然形如:\(n/2\) 个碳连成一棵树,然后还有一条边需要在一个碳中间连边,选任意一个碳使树连通即可。如果有两个碳都在其中连边,那么边数不对。
solution(D)
除去 BD 的情况,现在就是只有长度为 \(1,2\) 的置换环(下文将长度为 \(1\) 的置换环称为:氢)。可以稍微探讨一下结构,氢氢连边就是连一条边,碳碳连边就是连两条边两个方案,碳氢连边会使氢向碳的两端都连一条边(同时这个碳中间不能在连边,因为连通了)。碳碳氢可以连边,碳氢碳可以连边,碳碳碳碳氢氢氢氢碳碳碳碳可以连边,但是氢碳氢会连出环。那实际上就是我们先决定氢连成的树形态,再将碳一个一个插到氢树上(倒反天罡?),并且由于有氢将全树连通所以碳中间的边不需要再连。假设氢有 \(h\) 个,碳有 \(c\) 个,则氢连成的树形态有 \(h^{h-2}\) 种,碳插到氢树上的方案数打表可得(别尬黑,真能打表)\((h+2c)^{c-1}h\)。一个草率的证明是考虑 prufer 序列,假如有足够多的氢,钦定氢的编号比碳的编号都大,那么 prufer 序列中,前 \(c\) 个就是留给碳的,后 \(h-2\) 个留给氢。碳的部分,每个碳可能认父亲是另外一个碳(别忘了这里有两种连法)或者氢,但是最后一个留给碳的位置只能认氢作父亲,所以如此计算得到上式。氢只有 \(1\) 个时是边界情况,这时 prufer 序列只有 \(c-1\) 长,但是最后一个碳的父亲只能连向唯一的一个氢,也就是 \(\times 1\),因此不会对式子产生什么影响。
solution(BC)
即排列中不存在长度为 \(1, 2\) 的置换环。打表发现答案为 \(0\)。这是因为
- 两个长度为 \(a, b\) 的置换环,如果有一条边跨过这两个置换环,则能由它推导出共 \(\text{lcm}(a, b)\) 条边。证明从略。
- 两个长度为 \(a, b\) 的置换环,如果 \(\gcd(a, b)\) 不为 \(\min(a, b)\) 之中任何一个,则它们之间不能连边。因为此时 \(\text{lcm}(a, b)=ab/\gcd(a, b)\),钦定 \(a<b\),则因为 \(\gcd(a, b)\neq a\) 所以 \(a/\gcd(a, b)\geq 2\),所以 \(\text{lcm}(a, b)\geq 2b\geq a+b\) 超过一棵树应有的边数。推论:仅当 \(a|b\) 或 \(b|a\) 时可以连边。
- 除了碳中间的边,环内不能有树边。设环长为 \(x\),连一条第 \(1\) 个点到第 \(d\) 个点的边,如果 \(d\neq x/2\) 则推导出一个 \(x/\gcd(x, d)\) 的环,否则它现在不连通,环内不能再连边,向外界连边时,若连向 \(x\) 的真因数的环(\(\neq x\)),那么边的数量不对;若连向 \(x\) 的倍数的环,那么不会改变其不连通的事实,等于是救不回来了。
- 排列中不存在长度为 \(1, 2\) 的置换环,答案为 \(0\),因为这张图不连通的事实是救不回来的。
solution(general)
我们已经证明了足够多的引理。现在,我们只需要特判掉 \(n\leq 2\)、BD、D、BC 之后,从小到大依次枚举环长,将这个长度的所有置换环向它本身或者它的长度的真因数的其它所有置换环,像刚才碳上氢树一样,使用
计算这种置换环的贡献即可,其中 \(cct_i\) 是长度为 \(i\) 的置换环的个数。氢的树形态个数额外算一下就好了。最后答案是全部长度的答案乘起来。注意没有氢的时候需要碳生成初始树,同时要选一个碳在中间连边使最终树连通,同时不能再有其他奇数置换环否则它们会无树可插。
关于时间复杂度,你可以调和级数枚举,或者观察到 \(cct\) 只有 \(O(\sqrt n)\) 个值,直接 \(O((\sqrt n)^2)\) 暴力计算就能做到 \(O(n)\)。
code
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define endl "\n"
#define debug(...) void(0)
#endif
using LL = long long;
template <unsigned umod>
struct modint {/*{{{*/
static constexpr int mod = umod;
unsigned v;
modint() = default;
template <class T, enable_if_t<is_integral<T>::value, int> = 0> modint(const T& y) : v(y % mod + (y < 0 ? mod : 0)) {}
modint& operator+=(const modint& rhs) { v += rhs.v; if (v >= umod) v -= umod; return *this; }
modint& operator-=(const modint& rhs) { v -= rhs.v; if (v >= umod) v += umod; return *this; }
modint& operator*=(const modint& rhs) { v = (unsigned)(1ull * v * rhs.v % umod); return *this; }
modint& operator/=(const modint& rhs) { assert(rhs.v); return *this *= qpow(rhs, mod - 2); }
friend modint operator+(modint lhs, const modint& rhs) { return lhs += rhs; }
friend modint operator-(modint lhs, const modint& rhs) { return lhs -= rhs; }
friend modint operator*(modint lhs, const modint& rhs) { return lhs *= rhs; }
friend modint operator/(modint lhs, const modint& rhs) { return lhs /= rhs; }
template <class T> friend modint qpow(modint a, T b) {
modint r = 1;
for (; b; b >>= 1, a *= a) if (b & 1) r *= a;
return r;
}
friend int raw(const modint& self) { return self.v; }
friend ostream& operator<<(ostream& os, const modint& self) { return os << raw(self); }
};/*}}}*/
using mint = modint<998244353>;
template <class T> using pqueue = priority_queue<T, vector<T>, greater<T>>;
template <int N>
struct C_prime {
mint fac[N + 10], ifac[N + 10];
C_prime() {
fac[0] = 1;
for (int i = 1; i <= N; i++) fac[i] = fac[i - 1] * i;
ifac[N] = 1 / fac[N];
for (int i = N; i >= 1; i--) ifac[i - 1] = ifac[i] * i;
}
mint operator()(int n, int m) const { return n >= m ? fac[n] * ifac[m] * ifac[n - m] : 0; }
};
C_prime<500010> binom;
int n, per[500010], a[500010], fa[500010];
mint ans;
bool check() {
vector<int> deg(n + 1);
for (int i = 1; i <= n - 2; i++) deg[a[i]] += 1;
pqueue<int> q;
for (int i = 1; i <= n; i++) if (!deg[i]) q.push(i);
for (int i = 1; i <= n - 2; i++) {
int u = q.top(); q.pop();
fa[u] = a[i];
if (--deg[fa[u]] == 0) q.push(fa[u]);
}
fa[q.top()] = n;
fa[n] = 0;
auto ont = [&](int u, int v) { return fa[u] == v || fa[v] == u; };
for (int i = 1; i < n; i++) if (!ont(per[i], per[fa[i]])) return false;
return true;
}
void dfs(int now) {
if (now == n - 1) return ans += check(), void();
for (int i = 1; i <= n; i++) a[now] = i, dfs(now + 1);
}
void bruteforce() {
ans = 0;
dfs(1);
cout << ans << endl;
}
int cnt, cct[500010], s[500010];
vector<int> cyc[500010];
mint pfc[500010], pw2[500010];
void divide() {
cnt = 0;
vector<int> vis(n + 1);
for (int i = 1; i <= n; i++) cct[i] = 0;
for (int i = 1; i <= n; i++) if (!vis[i]) {
auto& vec = cyc[++cnt];
vec.clear();
for (int j = i; !vis[j]; j = per[j]) vec.push_back(j), vis[j] = true;
cct[vec.size()] += 1;
}
}
int mian() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> per[i];
if (n <= 2) return cout << 1 << endl, 0;
divide();
if (cct[1] == n) return cout << pfc[n] << endl, 0;
if (cct[2] * 2 == n) return debug("BD"), cout << pfc[n / 2] * pw2[n / 2 - 1] * (n / 2) << endl, 0;
if (!cct[1] && !cct[2]) return debug("BC"), cout << 0 << endl, 0;
if (cct[1] && cct[1] + cct[2] * 2 == n) {
debug("!B&D");
cout << qpow(mint{n}, cct[2] - 1) * cct[1] * pfc[cct[1]] << endl;
return 0;
}
//bruteforce();
mint ans = !cct[1] ? cct[2] : 1;
for (int i = 1; i <= n; i++) s[i] = 0;
for (int i = 1; i <= n; i++) if (cct[i]) {
for (int j = i; j <= n; j += i) s[j] += cct[i] * i;
if (s[i] > cct[i] * i) ans *= qpow(mint{s[i]}, cct[i] - 1) * (s[i] - cct[i] * i);
else if (i == 1) ans *= pfc[cct[1]];
else if (i == 2) ans *= pfc[cct[2]] * pw2[cct[2] - 1];
else ans = 0;
}
cout << ans << endl;
return 0;
}
int main() {
#ifndef LOCAL
cin.tie(nullptr)->sync_with_stdio(false);
#endif
pfc[1] = 1;
for (int i = 2; i <= 5e5; i++) pfc[i] = qpow(mint{i}, i - 2);
pw2[0] = 1;
for (int i = 1; i <= 5e5; i++) pw2[i] = pw2[i - 1] * 2;
int t;
cin >> t;
while (t--) mian();
return 0;
}
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/18440847