[Hard] P5900 无标号无根树计数
题目足够简要,无需简要题意,直接讲思路。
感觉符号化方法还没有普及,所以在这里介绍一下,大部分文段来自我自己在校内的《组合数学信息方法》讲稿,因此不构成侵权。
符号化方法
概述
在组合数学中,我们最常遇到的问题是,计算大小为 的某种东西的数量,这种东西同时也要满足一些限制。我们称这种可以计数的对象称为组合对象。全体组合对象称为组合类。一个组合类的 OGF,通常为其计数序列(大小对应数量)的 OGF。
在实际应用中,这些对象往往具有一些特征,如:
- 可以分割为若干个部分,每个部分对大小的贡献为 。
- 不同的部分通过经典的方法组合在一起,比如序列 、可重集合 、环 等等。
符号化方法用来将组合对象建模,使得其容易用生成函数来表示,进而用多项式相关算法来求解。
基础无标号类
我们称 为中性对象,并认为 ,表示其不会对大小产生贡献。中性对象的组合类 记作 ,其生成函数为 。
我们称 为原子对象,并认为 ,表示其对大小产生贡献为 。原子对象的组合类 ,记作 ,其生成函数为 。
对于两个组合对象的直积 ,有 ,对于两个不交的组合对象的并 ,有 。
这两个结论是显然的,本质上是加法原理和乘法原理。
无标号序列构造
定义 表示 中所有元素组成的序列的组合类,其生成函数为:
例子: 组合类 的生成函数 ,则其序列的生成函数为 。根据无穷级数的知识,可以得到 。
无标号可重集合构造
定义 表示 中所有元素组成的可重集合的组合类。
概念说清楚了,现在的问题是 的生成函数是什么?
考虑一个显然正确,但是不够通用的生成函数:
看到 不好处理,对其取 得:
根据 Taylor 展开的知识,我们有:
代入,得:
变形得:
取 得:
我们用 代替 表示无标号可重集合组合类的生成函数,并称 为 Euler 变换,如果你对 Pólya 定理熟悉,也许可以看出这是 Pólya 定理的扩展。
无标号有根树计数
第一部分
考虑定义 表示无标号有根树的组合类,则有:
其中原子对象组合类 表示树根,可重集合 表示子树。
构建 的生成函数 ,可以利用刚刚提到的 Euler 变换:
第二部分
两边取 :
两边求导:
变形,得:
令:
得到:
由于 ,得到:
变形得:
接下来考虑 ,大力求导与化简,得:
得到:
第三部分
你发现这玩意是一个半在线卷积的形式,我们可以考虑分治 FFT。
时间复杂度 。
但是我悲伤地发现,我根本就不会半在线卷积!于是我滚去学了一下。
无标号无根树计数
考虑钦定重心为根,则我们要去掉不以重心为根的情况。
先来考虑 ,此时重心是唯一的,由于有根树的重心,一定位于大小至少为 的子树内,于是可以将这个重子树看成一组,其余子树与根一起看成一组,则根不为重心的方案数为:
最后是 的情况,此时重心可能为两个点,而根据重心的性质,这两个点必定相邻,且断掉中间的那条边后,形成的两个连通块大小一定是 ,每个连通块一共有 种,所以要减去 。
最后写一下完整的式子:
总时间复杂度 可以通过本题。
代码
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353, g = 3, ginv = 332748118, inv2 = (mod + 1) >> 1;
const int N = 1e6 + 5;
int Add(int x, int y){ return (x + y) >= mod ? (x + y - mod) : (x + y); }
int Sub(int x, int y){ return (x - y) < 0 ? (x - y + mod) : (x - y); }
int Mul(int x, int y){ return 1ll * x * y % mod; }
int fastpow(int x, int y){
int ret = 1;
for(;y;y>>=1,x=Mul(x,x)){
if(y & 1) ret = Mul(ret, x);
}
return ret;
}
int Inv(int x){ return fastpow(x, mod-2); }
struct Poly{
vector<int> a;
Poly(){}
Poly(vector<int> _a):a(_a){}
Poly(int siz){ a.resize(siz); }
int& operator[](int x){ return a[x]; }
int size(){ return a.size(); }
Poly resize(int siz){ a.resize(siz, 0); return *this; }
void ntt(bool flag = 1);
static int init_ntt(int n, int m);
static Poly conv(Poly a, Poly b, function<int(int, int)> f);
};
int r[N], wt[N];
void Poly::ntt(bool flag){
int n = size();
for(int i=0;i<n;i++){
if(i < r[i]) swap(a[i], a[r[i]]);
}
for(int i=2;i<(n<<1);i<<=1){
int w1 = fastpow(flag ? g : ginv, (mod - 1) / i);
for(int j=0;j<n;j+=i){
int w = 1;
for(int k=0;k<(i>>1);k++,w=Mul(w,w1)){
int x = a[j + k], y = Mul(w, a[(i >> 1) + j + k]);
a[j + k] = Add(x, y), a[(i >> 1) + j + k] = Sub(x, y);
}
}
}
if(!flag){
int inv = Inv(n);
for(int i=0;i<n;i++) a[i] = Mul(a[i], inv);
}
}
int Poly::init_ntt(int n, int m){
int c = 1, l = 0;
while(c < (n + m)) c <<= 1, l++;
for(int i=0;i<=c;i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (l - 1));
return c;
}
Poly Poly::conv(Poly a, Poly b, function<int(int, int)> f){
int n = a.size(), m = b.size();
int c = Poly::init_ntt(n, m);
a.resize(c), b.resize(c);
a.ntt(), b.ntt();
for(int i=0;i<c;i++) a[i] = f(a[i], b[i]);
a.ntt(0), a.resize(n + m + 1);
return a;
}
Poly operator*(Poly a, Poly b){ return Poly::conv(a, b, Mul); }
int F[N], G[N], n;
void solve(int l, int r){
if(l == r){
F[l] = (l == 1) ? 1 : Mul(F[l], Inv(l - 1));
for(int i=l;i<=n;i+=l) G[i] = Add(G[i], Mul(l, F[l]));
return;
}
int mid = (l + r) >> 1;
solve(l, mid);
Poly a(mid - l + 1), b(r - l + 1);
for(int i=l;i<=mid;i++) a[i - l] = F[i];
for(int i=1;i<=(l==1?mid:r-l);i++) b[i] = G[i];
a = a * b;
for(int i=mid+1;i<=r;i++) F[i] = Add(F[i], a[i - l]);
if(l != 1){
a.resize(mid - l + 1), b.resize(r - l + 1);
for(int i=1;i<=r-l;i++) b[i] = F[i];
for(int i=l;i<=mid;i++) a[i - l] = G[i];
a = a * b;
for(int i=mid+1;i<=r;i++) F[i] = Add(F[i], a[i - l]);
}
solve(mid + 1, r);
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin >> n;
solve(1, n);
int ans = F[n];
for(int k=((n>>1)+1);k<n;k++) ans = Sub(ans, Mul(F[k], F[n - k]));
if(!(n & 1)) ans = Sub(ans, Mul(Sub(F[n >> 1], 1), Mul(inv2, F[n >> 1])));
cout << ans << '\n';
return 0;
}
// Written by xiezheyuan