题解 P5900 【无标号无根树计数】

Karry5307

2020-03-26 10:08:57

Solution

### 题意 求 $n$ 个点的无标号无根树数量。 $\texttt{Data Range:}1\leq n\leq 2\times 10^5$ ### 题解 可能是你谷最详细阐明 $\texttt{Euler}$ 变换的题解了。 首先考虑有根树计数,设 $f_n$ 是 $n$ 个点的无标号有根树数量,那么我们可以钦定某一个点为根,剩下的子树的大小加起来必须为 $n-1$,会发现是个背包。于是考虑使用生成函数优化一下: $$f_n=[x^{n-1}]\prod\limits_{i=1}^{\infty}\left(\frac{1}{1-x^i}\right)^{f_i}$$ 现在我们来定义 $F(x)$ 的 $\texttt{Euler}$ 变换为 $$\mathcal{E}(F(x))=\prod\limits_{i=1}^{\infty}\left(\frac{1}{1-x^i}\right)^{f_i}$$ 这里给出另外一种组合解释,如果 $F(x)$ 为 $n$ 个点满足某种性质的无标号连通图数量,那么 $\mathcal{E}(F(x))$ 为 $n$ 个点满足这种性质的无标号图的数量,这个可以像上面那样,枚举连通块的个数做背包得到,类似于有标号图计数中的 $\exp$。 但是这个定义式是可以优化的,如果你做过付公主的背包这个题目的话应该对这种优化比较熟悉。我们设 $G(x)=\mathcal{E}(F(x))$,那么对定义式等式两端取对数,有 $$\ln G(x)=-\sum\limits_{i=1}^{\infty}f_i\ln(1-x^i)$$ 两边求导,得到 $$\frac{G^\prime(x)}{G(x)}=\sum\limits_{i=1}^{\infty}f_i\frac{ix^{i-1}}{1-x^i}$$ 做个级数展开 $$\frac{G^\prime(x)}{G(x)}=\sum\limits_{i=1}^{\infty}f_i\sum\limits_{j=0}^{\infty}ix^{(j+1)i-1}$$ 积分回来 $$\ln G(x)=\sum\limits_{i=1}^{\infty}f_i\sum\limits_{j=1}^{\infty}\frac{x^{ij}}{j}$$ 交换求和顺序,可以写成这个样子 $$\ln G(x)=\sum\limits_{j=1}^{\infty}\frac{1}{j}\sum\limits_{i=1}^{\infty}f_i(x^{j})^i$$ 注意到右边是个点值,所以 $$\ln G(x)=\sum\limits_{j=1}\frac{F(x^j)}{j}$$ 于是有 $$\mathcal{E}(F(x))=\exp\left(\sum\limits_{j=1}\frac{F(x^j)}{j}\right)$$ 回到无标号有根数的计数上面来,所以我们用生成函数表示可以得到以下式子 $$F(x)=x\cdot\mathcal{E}(F(x))$$ 这个可以牛迭求出,尽管是 $O(n\log n)$ 的,但是常数非常大,实际上可能比不过 $O(n\log^2n)$ 的分治 $\texttt{FFT}$。 考虑同时求导并且乘上 $x$,也就是使用 $\vartheta$ 算子,可以得到(~~计算过程给读者留做练习~~) $$\vartheta F(x)=F(x)+F(x)\left(\sum\limits_{i=1}^{\infty}x^iF^\prime(x^i)\right)$$ 设 $G(x)=\sum\limits_{i=1}^{\infty}x^iF^\prime(x^i)$,他的系数可以通过~~找规律~~和~~目瞪口呆法~~发现,有 $$g_n=\sum\limits_{d\mid n}df_d$$ 于是有 $$nf_n=f_n+\sum\limits_{i}f_{i}g_{n-i}$$ 解出来得到 $$f_n=\frac{1}{n-1}\sum\limits_{i}f_{i}g_{n-i}$$ 这个可以分治 $\texttt{FFT}$ 解决,虽说是 $O(n\log^2n)$ 的,但是常数很小。 接下来考虑把根的影响去掉,这个的话可以考虑枚举重心,用有根树的答案减去根不是重心的答案。 由于当树的大小是偶数的时候可能存在两个重心,所以要分类讨论一下。 当 $n$ 为奇数的时候我们可以不用想那么多,由于已经钦定了根不是重心,那么必然有一棵子树使得它的大小大于 $\lfloor\frac{n}{2}\rfloor$,设为 $k$,枚举一下这个 $k$,答案为 $$f_n-\sum\limits_{k=\lfloor\frac{n}{2}\rfloor+1}f_kf_{n-k}$$ 右边的东西可以随手卷一下。 当 $n$ 为偶数的时候有可能存在两个重心,且其中一个是根,枚举并判掉。 额外减掉的方案为 $$\binom{f_{\frac{n}{2}}}{2}$$ 至此,这个题目就可以用 $O(n\log n)$ 或者 $O(n\log^2n)$ 的时间复杂度内解决。 ### 代码 ```cpp #include<bits/stdc++.h> #pragma GCC optimize("Ofast,unroll-loops") using namespace std; typedef int ll; typedef long long int li; typedef unsigned long long int ull; const ll MAXN=524291,MOD=998244353,G=3,INVG=332748118; ll n,cnt,limit; ll f[MAXN],g[MAXN],tmp[MAXN],tmp2[MAXN],tmp3[MAXN],rev[MAXN<<1],omgs[MAXN<<1]; inline ll read() { register ll num=0,neg=1; register char ch=getchar(); while(!isdigit(ch)&&ch!='-') { ch=getchar(); } if(ch=='-') { neg=-1; ch=getchar(); } while(isdigit(ch)) { num=(num<<3)+(num<<1)+(ch-'0'); ch=getchar(); } return num*neg; } inline ll qpow(ll base,ll exponent) { li res=1; while(exponent) { if(exponent&1) { res=1ll*res*base%MOD; } base=1ll*base*base%MOD,exponent>>=1; } return res; } inline void setupOmg(ll cnt) { ll limit=log2(cnt)-1,omg; omg=qpow(G,(MOD-1)>>(limit+1)),omgs[cnt>>1]=1; for(register int i=(cnt>>1|1);i!=cnt;i++) { omgs[i]=(li)omgs[i-1]*omg%MOD; } for(register int i=(cnt>>1)-1;i;i--) { omgs[i]=omgs[i<<1]; } } inline void NTT(ll *cp,ll cnt,ll inv) { static ull tcp[MAXN]; register ll cur=0,x,shift=log2(cnt)-__builtin_ctz(cnt); if(inv==-1) { reverse(cp+1,cp+cnt); } for(register int i=0;i<cnt;i++) { tcp[rev[i]>>shift]=cp[i]; } for(register int i=2;i<=cnt;i<<=1) { cur=i>>1; for(register int j=0;j<cnt;j+=i) { for(register int k=0;k<cur;k++) { x=tcp[j|k|cur]*omgs[k|cur]%MOD; tcp[j|k|cur]=tcp[j|k]+MOD-x,tcp[j|k]+=x; } } } for(register int i=0;i<cnt;i++) { cp[i]=tcp[i]%MOD; } if(inv==1) { return; } x=MOD-(MOD-1)/cnt; for(register int i=0;i<cnt;i++) { cp[i]=(li)cp[i]*x%MOD; } } inline void conv(ll cnt,ll *f,ll *g,ll *res) { static ll tmpf[MAXN],tmpg[MAXN]; for(register int i=0;i<cnt;i++) { tmpf[i]=f[i],tmpg[i]=g[i],res[i]=0; } ll limit=log2(cnt)-1; for(register int i=0;i<cnt;i++) { rev[i]=(rev[i>>1]>>1)|((i&1)<<limit); } NTT(tmpf,cnt,1),NTT(tmpg,cnt,1); for(register int i=0;i<cnt;i++) { res[i]=(li)tmpf[i]*tmpg[i]%MOD; tmpf[i]=tmpg[i]=0; } NTT(res,cnt,-1); } inline void calc(ll l,ll r) { ll mid=(l+r)>>1; if(l==1) { for(register int i=l;i<=mid;i++) { tmp[i-l]=f[i],tmp2[i-l]=g[i]; } ll cnt=1; while(cnt<=((r-l)<<1)) { cnt<<=1; } conv(cnt,tmp,tmp2,tmp3); for(register int i=mid+1;i<=r;i++) { f[i]=(f[i]+tmp3[i-l-1])%MOD; } for(register int i=0;i<cnt;i++) { tmp[i]=tmp2[i]=tmp3[i]=0; } return; } for(register int i=l;i<=mid;i++) { tmp[i-l]=f[i]; } for(register int i=1;i<=r-l;i++) { tmp2[i-1]=g[i]; } ll cnt=1; while(cnt<=((r+mid-l-l+1)<<1)) { cnt<<=1; } conv(cnt,tmp,tmp2,tmp3); for(register int i=mid+1;i<=r;i++) { f[i]=(f[i]+tmp3[i-l-1])%MOD; } for(register int i=0;i<cnt;i++) { tmp[i]=tmp2[i]=tmp3[i]=0; } for(register int i=l;i<=mid;i++) { tmp[i-l]=g[i]; } for(register int i=1;i<=r-l;i++) { tmp2[i-1]=f[i]; } conv(cnt,tmp,tmp2,tmp3); for(register int i=mid+1;i<=r;i++) { f[i]=(f[i]+tmp3[i-l-1])%MOD; } for(register int i=0;i<cnt;i++) { tmp[i]=tmp2[i]=tmp3[i]=0; } } inline void cdqFFT(ll l,ll r) { if(l==r) { f[l]=l!=1?((li)f[l]*qpow(l-1,MOD-2)%MOD):1; for(register int i=l;i<=n;i+=l) { g[i]=(g[i]+(li)f[l]*l%MOD)%MOD; } return; } ll mid=(l+r)>>1; cdqFFT(l,mid),calc(l,r),cdqFFT(mid+1,r); } int main() { n=read()+1,cnt=1; while(cnt<=n) { cnt<<=1; } setupOmg(cnt<<1),cdqFFT(1,cnt),cnt=1,limit=-1; while(cnt<=(n<<1)) { cnt<<=1,limit++; } for(register int i=0;i<cnt;i++) { rev[i]=(rev[i>>1]>>1)|((i&1)<<limit); tmp[i]=i<=n?f[i]:0; } NTT(tmp,cnt,1); for(register int i=0;i<cnt;i++) { tmp[i]=(li)tmp[i]*tmp[i]%MOD; } NTT(tmp,cnt,-1); for(register int i=1;i<=n;i+=2) { g[i]=(f[i]-(li)tmp[i]*499122177%MOD+MOD)%MOD; } for(register int i=2;i<=n;i+=2) { tmp[i]=(tmp[i]-(li)f[i>>1]*f[i>>1]%MOD+MOD)%MOD; g[i]=(f[i]-(li)tmp[i]*499122177%MOD+MOD)%MOD; g[i]=(g[i]-(li)f[i>>1]*(f[i>>1]-1)%MOD*499122177%MOD+MOD)%MOD; } printf("%d\n",g[n-1]); } ```