[BZOJ3625]小朋友和二叉树
壹、题目描述
贰、题解
只要两颗二叉树不全等,他们就不同。
设 \(f_i\) 表示一棵神犇二叉树,它的权值之和为 \(i\) 的方案数,设 \(T=\{c_1,c_2,...c_n\}\),那么
边界 \(f_0=1\),因为题目说明空树也算一种方案。
对于 \(F\) 我们写出它的生成函数:
发现后是卷积形式,但是前面的 \(s\in T\) 不是连续的,我们考虑定义 \(g_i=[i\in T]\),那么就有
如果我们定义 \(G(x)\) 为 \(g\) 的生成函数,不难看出 \(F(x)\) 就是:
先来两个 \(F(x)\) 卷在一起,再来一个 \(G(x)\) 和两个 \(F(x)\) 卷在一起的结果再卷一卷,其实就是
然后,可以得到
其中,有 \(G(0)=0,F(0)=1\),考虑
所以有
而我们最后要求的是
但是我们发现一个大问题,由于 \(G(x)\) 的常数项是 \(0\),所以分母那一坨是没有逆元的,怎么办呢,我们考虑分子有理化,上下同时乘 \(1+\sqrt{1-4G(x)}\),得到
对于这个东西,我们只需要多项式开根和多项式逆元即可。
时间复杂度 \(\mathcal O(n\log^2 n)\).
叁、代码
\(\color{red}{\text{talk is treap, show you the code}.}\)
using namespace Elaina;
const int mod=998244353;
const int g=3;
const int gi=332748118;
const int maxn=1e5;
const int maxc=1e5;
const int maxsize=131072<<2;
const int inv2=499122177;
inline int qkpow(int a, int n){
int ret=1;
for(; n>0; n>>=1, a=1ll*a*a%mod) if(n&1)
ret=1ll*ret*a%mod;
return ret;
}
// first of all you should invoke function initial in the main function
namespace NTT{
int rev[maxsize+5];
int G[2][55], n, invn;
inline void initial(){
for(int j=1; j<=50; ++j){
G[0][j]=qkpow(g, (mod-1)/(1<<j));
G[1][j]=qkpow(gi, (mod-1)/(1<<j));
}
}
inline void prepare(const int len){
for(n=1; n<len; n<<=1);
invn=qkpow(n, mod-2);
for(int i=1; i<n; ++i)
rev[i]=(rev[i>>1]>>1)|((i&1)?(n>>1):0);
}
inline void ntt(vector<int>&f, const int opt){
f.resize(n);
for(int i=0; i<n; ++i) if(i<rev[i])
swap(f[i], f[rev[i]]);
for(int p=2, cnt=1; p<=n; p<<=1, ++cnt){
int len=p>>1, w=G[opt][cnt];
for(int k=0; k<n; k+=p){
int buf=1;
for(int i=k; i<k+len; ++i, buf=1ll*buf*w%mod){
int tmp=1ll*buf*f[i+len]%mod;
f[i+len]=(f[i]-tmp+mod)%mod;
f[i]=(f[i]+tmp)%mod;
}
}
}
if(opt==1) for(int i=0; i<n; ++i)
f[i]=1ll*f[i]*invn%mod;
}
}
// len should be a pow of 2
namespace poly{
vector<int> inv(vector<int>f, const int len){
f.resize(len);
vector<int>h; h.resize(len);
if(len==1){
h[0]=qkpow(f[0], mod-2);
return h;
}
h=inv(f, len>>1);
NTT::prepare(len<<1);
NTT::ntt(h, 0), NTT::ntt(f, 0);
for(int i=0; i<NTT::n; ++i)
h[i]=(2ll*h[i]%mod+mod-1ll*f[i]*h[i]%mod*h[i]%mod)%mod;
NTT::ntt(h, 1); h.resize(len);
return h;
}
vector<int> sqrt(vector<int>f, const int len){
f.resize(len);
vector<int>h; h.resize(len);
// 因为是 1-4G(x) 调用, 常数是 1
if(len==1){h[0]=1; return h;}
h=sqrt(f, len>>1);
vector<int>invh=inv(h, len);
NTT::prepare(len<<1);
NTT::ntt(f, 0); NTT::ntt(h, 0); NTT::ntt(invh, 0);
for(int i=0; i<NTT::n; ++i)
h[i]=(1ll*f[i]*invh[i]%mod*inv2%mod+1ll*h[i]*inv2%mod)%mod;
NTT::ntt(h, 1);
return h;
}
}
int n, m;
int t[maxn+5];
vector<int>G;
inline void input(){
n=readin(1), m=readin(1);
int c;
for(int i=1; i<=n; ++i){
c=readin(1);
t[c]=1;
}
for(int i=0; i<=maxc; ++i)
G.push_back(t[i]);
}
signed main(){
NTT::initial();
input();
int N=maxc+1;
int len; for(len=1; len<N; len<<=1);
for(int i=0; i<N; ++i) G[i]=(mod-4ll*G[i]%mod)%mod;
++G[0];
G=poly::sqrt(G, len);
G.resize(N); ++G[0];
G=poly::inv(G, len); G.resize(N);
for(int i=1; i<=m; ++i)
writc((G[i]+G[i])%mod,'\n');
return 0;
}
肆、用到の小 \(\tt trick\)
把多项式求逆元和开根都推一下。
多项式求逆
要求 \(F(x)G(x)\equiv 1\pmod {x^{2n}}\).
假设已知 \(F(x)H(x)\equiv 1\pmod {x^n}\),那么有
边界就是常数项的逆元,这个东东用倍增。
多项式开根
前提是常数项必须存在二次剩余。
我们要求 \(G^2(x)\equiv F(x)\pmod{x^{2n}}\).
假设已知 \(H^2(x)\equiv F(x)\pmod{x^n}\),那么有(下文简写函数)
边界就是常数项开根(同余意义下使用二次剩余),这个东东用倍增。
另外
对于这类较为抽象的问题,我们可以考虑先将所有答案看做一个答案序列,然后列出转移方程,发现可以使用生成函数之后才使用生成函数,而不是直接使用,一般生成函数没有那么直接。