大砖数学(三)生成函数和多项式全家桶

· · 算法·理论

前言

行列式被推到第四篇去了,这里是插播。

后缀算法原定于本篇之前更新,但是好像赶不出进度,就先搁置了。

我爱积积本来要更新的,但是本来本来,没有本来的啦!

后记:后缀算法更完了,这篇还没开始写。

很多东西可能来自这篇,给 wangyizhi 一个二作,但是他退役了,那我就放心贺了

生成函数

生成函数部分为幼儿园小班难度,如果您已经上过幼儿园小班,建议跳过。

我不知道怎么开头。

生成函数(Generating Function,又称母函数),是利用多项式的各项指数和系数来表示状态和结果的函数。

骗你的,我也不知道这东西标准定义是啥。

我们可以利用指数来表示状态,用函数之间的运算来表示状态的转移,最终对应项的系数就是我们想要的结果,一般来说,这个状态是一个数。

普通生成函数

普通生成函数(OGF)可以把数列转化成多项式。具体的,序列 \{a_n\} 的 OGF 为

F(x)=\sum_{i=0}^n a_ix_i

如果数列无限长,生成函数就变成了幂级数,例如

F(x)=1+x+x^2+\cdots

幂级数有封闭形式,即形式幂级数意义下的恒等,以 F(x)=1+x+x^2+\cdots 为例:

F(x)&=1+x+x^2+\cdots \\ xF(x)&=x+x^2+x^3+\cdots \\ (1-x)F(x)&=1 \\ F(x)&=\frac{1}{1-x} \end{aligned}

生成函数一个比较重要的性质是,它的乘法相当于序列的卷积,即

\operatorname{OGF}(a*b)=\operatorname{OGF}(a)\times \operatorname{OGF}(b)

其中

(a*b)_i=\sum_{j=0}^ia_jb_{i-j}

生成函数可以用作有递推式的情况下求通项。例如,斐波那契数列:

我们有递推式 f_i=f_{i-1}+f_{i-2},不妨设 F(x)=\operatorname{OGF}(f),有

F(x)=xF(x)+x^2F(x)+x

解得

F(x)=\frac{x}{1-x-x^2}

F(x)=\frac A{1-ax} + \frac B{1-bx}=\frac{(A+B)-(Ab+Ba)x}{1-(a+b)x+abx^2}

\begin{cases} A+B=0 \\ Ab+Ba=-1 \\ a+b=1 \\ ab=-1 \end{cases}

解得

\begin{cases} A=\frac 1 {\sqrt 5} \\ B=-\frac 1 {\sqrt 5} \\ a=\frac{1+\sqrt 5}{2} \\ b=\frac{1-\sqrt 5}{2} \end{cases}

又由于

\frac{A}{1-ax}=\sum_{i\ge 0}Aa^ix^i

所以

F(x)=\sum_{i=0} \frac 1{\sqrt 5} \left( \left( \frac{1+\sqrt 5}{2}\right)^i - \left( \frac{1-\sqrt 5}{2}\right)^i \right) x^i

f_n=\frac 1{\sqrt 5} \left( \left( \frac{1+\sqrt 5}{2}\right)^n - \left( \frac{1-\sqrt 5}{2}\right)^n \right)

指数型生成函数

定义数列 \{a_n\} 的指数型生成函数(EGF)为

\hat F(x)=\sum_{i=0}^n a_i \frac{x^i}{i!}

考虑一个经典问题:

n 个有标号的求染成红色或蓝色,求方案数。

设其中有 i 个红球的方案数为 f_i,有 i 个蓝球的方案为 g_i,则答案为

h_n&=n! \sum_{i+j=n} \binom{n}{i} f_ig_j \\ &=n! \sum_{i+j=n} \frac{f_i}{i!} \cdot \frac{g_j}{j!} \end{aligned}

\frac{h_n}{n!}=\sum_{i+j=n} \frac{f_i}{i!} \cdot \frac{g_j}{j!}

\hat F(x)=\operatorname{EGF}(f),\hat G(x)=\operatorname{EGF}(g),\hat H(x)=\operatorname{EGF}(h),则

\hat H(x)=\hat F(x) \hat G(x)

结论可以推广到多个数列的情况,我们有

\hat G(x)=\prod_i \hat F_i(x)

g_n=\sum_{\sum a_i=n} \binom{n}{a_1,a_2,\dots,a_n}\prod f_{i,a_i}

等价。

另外,对 e^x 做泰勒展开,有

e^x=\sum_{i=0}^\infty \frac{x^i}{i!}=\operatorname{EGF}(\{1,1,1,\dots\})

EGF 的微分(导数)也有其组合意义。

\hat F(x)=\sum_{i\ge 0} a_i \frac{x^i}{i!}

\hat F'(x)=\sum_{i\ge 1}a_i \frac{ix^{i-1}}{i!}=\sum_{i \ge 0} a_{i+1} \frac{x^i}{i!}

相当于给每项都乘上了 i+1,也就是说,我们可以将其理解为有 i+1 个有标号元素的结构,从 i+1 个元素中选取一个,也就是说,这是在结构中标记了一个元素的方案数,于是我们可以将其用作含有某个特定元素的计数。

二元生成函数

一个变量不够用,就再引入一个,到后面消元。

多项式全家桶

多项式全家桶部分为幼儿园小中班衔接难度,如果您已经上过幼儿园中班或上过小中班衔接,建议跳过。

任意模数多项式乘法

我不会啊。

但我们有三模 NTT。

泰勒展开

设函数 f 在点 a 的某邻域内具有足够阶导数,则:

f(x)=\sum_{k=0}^n \frac{f^{(k)}(a)}{k!}(x-a)^k+o((x-a)^n)

邻域,即 (a-\epsilon,a+\epsilon)

这里的 o((x-a)^n) 表示比 (x-a)^n 小一个量级。

多项式牛顿迭代

已知二元函数 G(x,y),求一多项式 G(x,f(x))\equiv 0 \pmod {x^n}

考虑倍增,先对 n=1 求出答案。

现在假设我们求出了模 x^k 意义下的解 f',将 G(x,f(x))f(x)f(x)=f'(x) 处做一阶泰勒展开,令 f(x)=f'(x)+\Delta(x),显然 \Delta(x)x^k 量级,有

G(x,f'+\Delta)=G(x,f')+\frac{\partial G}{\partial y}(x,f'(x))\cdot \Delta+O(\Delta^2) 忽略 $O(\Delta^2)$,这样精度在 $x^{2k}$,即精度平方,并令 $G(x,f(x))$ 为 $0$: $$\begin{aligned} G(x,f')+G_y(x,f')\cdot \Delta &= 0 \\ \Delta &= -\frac{G(x,f')}{G_y(x,f')} \end{aligned}$$ 于是 $$f(x) \equiv f'(x)-\frac{G(x,f')}{G_y(x,f')} \pmod {x^{2k}}$$ 这里用 $G_y$ 表示了上述偏导。 如此迭代,$O(\log n)$ 次即可求出 $f(x)$。 ### 多项式乘法逆 使用多项式牛顿迭代。 设给定函数为 $h(x)$,则 $G(x,y)=\frac{1}{y}-h(x)$。设 $f'(x)$ 为求出的模 $x^{\lceil \frac{n}{2} \rceil}$ 意义下的答案,我们有 $$\begin{aligned} f(x)&\equiv f'(x)-\frac{\frac{1}{f'(x)}-h(x)}{-\frac{1}{f'^2(x)}} \\ &\equiv 2f'(x) - f'^2(x)h(x) \pmod {x^n} \end{aligned}$$ 考虑时间复杂度。由 master 定理,每次都要做 FFT,得到 $$T(n)=T(\frac{n}{2})+O(n\log n)=O(n\log n)$$ 总时间复杂度 $O(n\log n)$。 :::info[参考代码] ```cpp void invNTT(int f[],int ans[],int tmpa[],int rev[],int n,int mod,int G) { if(n==1) { ans[0]=quick_pow(f[0],mod-2,mod); return; } invNTT(f,ans,tmpa,rev,n+1>>1,mod,G); int l=-1,lim=1; while(lim<=(n<<1)) { lim<<=1; l++; } for(int i=0;i<=lim;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<l); for(int i=0;i<n;i++) tmpa[i]=f[i]; for(int i=n;i<=lim;i++) tmpa[i]=0; NTT(tmpa,rev,lim,1,mod,G); NTT(ans,rev,lim,1,mod,G); for(int i=0;i<=lim;i++) ans[i]=(2-tmpa[i]*ans[i]%mod+mod)%mod*ans[i]%mod; NTT(ans,rev,lim,-1,mod,G); for(int i=n;i<=lim;i++) ans[i]=0; } ``` ::: ### 多项式 ln 设给定的函数为 $f(x)$,考虑先微分再积分,有 $$\begin{aligned} \frac{\text{d} \ln f(x)}{\text{d} x}&\equiv \frac{f'(x)}{f(x)} \\ \text{d} \ln f(x) &\equiv \frac{f'(x)}{f(x)} \text{d} x \\ \ln f(x)&\equiv \int \frac{f'(x)}{f(x)} \text{d} x \pmod {x^n} \end{aligned}$$ 求导求逆乘法积分,做完了,时间复杂度 $O(n\log n)$。 :::info[参考代码] ```cpp void devNTT(int f[],int g[],int n,int mod) { for(int i=1;i<n;i++) g[i-1]=i*f[i]%mod; g[n-1]=0; } void lnNTT(int f[],int g[],int n,int mod,int G) { memset(a,0,sizeof a); memset(b,0,sizeof b); devNTT(f,a,n,mod); invNTT(f,b,tmpa,rev,n,mod,G); int l=-1,lim=1; while(lim<=(n<<1)) { lim<<=1; l++; } for(int i=0;i<=lim;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<l); NTT(a,rev,lim,1,mod,G); NTT(b,rev,lim,1,mod,G); for(int i=0;i<=lim;i++) a[i]=a[i]*b[i]%mod; NTT(a,rev,lim,-1,mod,G); for(int i=1;i<n;i++) g[i]=a[i-1]*quick_pow(i,mod-2)%mod; g[0]=0; } ``` ::: ### 多项式 exp 设给定函数为 $h(x)$,则 $G(x,y)=\ln y-h(x)$。设 $f'(x)$ 为求出的模 $x^{\lceil \frac{n}{2} \rceil}$ 意义下的答案,我们有 $$\begin{aligned} f(x)&\equiv f'(x)-\frac{\ln f'(x)-h(x)}{\frac{1}{f'(x)}} \\ &\equiv f'(x)(1-\ln x+h(x)) \pmod {x^n} \end{aligned}$$ 求 $\ln$ 加法乘法迭代,复杂度分析同求逆,时间复杂度 $O(n\log n)$。 :::info[参考代码] ```cpp void expNTT(int f[],int g[],int n,int mod,int G) { if(n==1) { g[0]=1; return; } expNTT(f,g,n+1>>1,mod,G); lnNTT(g,tmpb,n,mod,G); // 1-ln(F_0(x)) tmpb[0]=(f[0]+1-tmpb[0]+mod)%mod; for(int i=1;i<n;i++) tmpb[i]=(f[i]-tmpb[i]+mod)%mod; int l=-1,lim=1; while(lim<=(n<<1)) { lim<<=1; l++; } for(int i=0;i<=lim;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<l); NTT(tmpb,rev,lim,1,mod,G); NTT(g,rev,lim,1,mod,G); for(int i=0;i<=lim;i++) g[i]=g[i]*tmpb[i]%mod; NTT(g,rev,lim,-1,mod,G); for(int i=n;i<=lim;i++) g[i]=tmpb[i]=0; } ``` ::: ### 多项式快速幂 我们有 $$f(x)^k = \exp (k \ln f(x))$$ 做完了,时间复杂度 $O(n\log n)$。 :::info[参考代码] ```cpp INLINE void powNTT(int *f,int *g,int n,const string &K) { static int h[N]; int tmpk=0,tk=0,k=0; for(auto ch:K) { k=((k<<3)+(k<<1)+(ch^48))%mod; tk=((tk<<3)+(tk<<1)+(ch^48))%(mod-1); tmpk=min(1000000,(tmpk<<3)+(tmpk<<1)+(ch^48)); } int c=0; while(f[c]==0&&c<n) c++; if((f[0]==0&&tmpk>=n)||c*k>=n) { for(int i=0;i<n;i++) g[i]=0; return; } int tmpn=n; for(int i=c;i<n;i++) f[i-c]=f[i]; for(int i=n-c+1;i<n;i++) f[i]=0; n-=c; int iv=quick_pow(f[0],mod-2),v=quick_pow(f[0],tk); for(int i=0;i<n;i++) f[i]=1ll*f[i]*iv%mod; lnNTT(f,g,n); for(int i=0;i<n;i++) g[i]=1ll*g[i]*k%mod; expNTT(g,h,n); for(int i=0;i<c*k;i++) g[i]=0; for(int i=c*k;i<tmpn;i++) g[i]=1ll*h[i-c*k]*v%mod; } ``` ::: ### 多项式开方 一种思路是 $\sqrt {f(x)} = f(x) ^{1/2}$,然后按照快速幂的方法求解。 当然,也可以多项式牛顿迭代,设给定函数为 $h(x)$,则 $G(x,y)=y^2-h(x)$。设 $f'(x)$ 为求出的模 $x^{\lceil \frac{n}{2} \rceil}$ 意义下的答案,我们有 $$\begin{aligned} f(x)&\equiv f'(x)-\frac{f'^2(x)-h(x)}{2 f'(x)} \\ &\equiv \frac{f'(x)+h(x)}{2f'(x)} \pmod {x^n} \end{aligned}$$ 时间复杂度 $O(n\log n)$。 :::info[参考代码] ```cpp int quadrasique(int x) { mt19937 mt(time(0)); int s=mt()%(mod-1)+1; while(quick_pow(w=(s*s%mod-x+mod)%mod,mod-1>>1)<=1) s=mt()%(mod-1)+1; fcomplex<int> tmp=fcomplex<int>(s,1); tmp=quick_pow(tmp,mod+1>>1); return min(tmp.x,mod-tmp.x); } void sqrtNTT(int f[],int g[],int n) { static int h[N]; if(n==1) { g[0]=quadrasique(f[0]); assert(g[0]*g[0]%mod==f[0]); return; } sqrtNTT(f,g,n+1>>1); int l=-1,lim=1; while(lim<=(n<<1)) { lim<<=1; l++; } for(int i=0;i<lim;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<l); invNTT(g,h,rev,n); NTT(g,rev,lim,1); NTT(h,rev,lim,1); for(int i=0;i<lim;i++) g[i]=g[i]*g[i]%mod; NTT(g,rev,lim,-1); for(int i=n;i<lim;i++) g[i]=0; for(int i=0;i<n;i++) g[i]=(g[i]+f[i])%mod; NTT(g,rev,lim,1); for(int i=0;i<lim;i++) g[i]=g[i]*inv2%mod*h[i]%mod; NTT(g,rev,lim,-1); for(int i=n;i<lim;i++) g[i]=0; for(int i=0;i<lim;i++) h[i]=0; } ``` :::