多项式与生成函数学习笔记
多项式
记号
为了方便描述,用大写字母表示一个多项式,用其对应的小写字母表示该多项式某一项的系数:
求导
只有我傻傻不会求导,所以放几个常见导数。
链式法则:
泰勒展开
把 (F(x)) 在 (F(x_0)) 处泰勒展开:
牛顿迭代
给定函数 (G) ,找出多项式 (F(x)) 使得:
考虑倍增,已知 (G(F_n(x))equiv 0 pmod{x^n}) ,求 (F_{2n}(x)) 满足 (G(F_{2n}(x))equiv 0pmod{x^{2n}}) 。
把 (G(F_{2n}(x))) 在 (G(F_{n}(x))) 处做泰勒展开:
后面的项和左边的项在 (pmod {x^{2n}}) 意义下都是 (0) :
多项式求逆
给定 (F(x)) ,求出 (P(x)) 满足 (F(x)P(x)equiv 1 pmod {x^n}) 。
考虑倍增,设 (F_n(x)P(x)equiv 1pmod{x^n}) ,那么:
时间复杂度: (mathcal O(nlog_2n)) 。
暴力
时间复杂度: (mathcal O(n^2)) 。
多项式除法
给定 (A(x),B(x)) ,求 (D(x),R(x)) 满足 (A(x)=D(x)B(x)+R(x)) 并且 (deg(R(x))<deg(B(x))) 。
设 (deg(A(x))=n,deg(B(x))=m) ,则 (deg(D(x))=n-m,deg(R(x))<m) ,不妨设 (deg(R(x))=m-1) 。
记 (F^R(x)=x^{deg(F(x))}F(frac{1}{x})) ,也就是将 (F) 的系数反转得到的多项式。
原式也可以写成:
因为 (deg(D(x))=n-m<n-m+1) ,所以求出来的 (D(x)) 是真实的,由此可以求出 (R(x)) 。
时间复杂度: (mathcal O(nlog_2n)) 。
多项式 ln
给定 (F(x)) ,求出 (P(x)) 满足 (P(x)equiv ln(F(x))pmod{x^n}) 。
两边同时求导:
(P(x)) 常数项为 (0) , (F(x)) 常数项为 (1) 。(不为 (1) 不收敛)
时间复杂度: (mathcal O(nlog_2 n)) 。
暴力
时间复杂度: (mathcal O(n^2)) 。
多项式 exp
给定 (F(x)) ,求出 (P(x)) 满足 (P(x)equiv e^{F(x)}pmod{x^n}) 。
套牛顿迭代, (G(X)=ln X-F(x)) 。
(F(x)) 常数项为 (0) , (P(x)) 常数项为 (1) 。
时间复杂度: (mathcal O(nlog_2n)) 。
暴力
时间复杂度: (mathcal O(n^2)) 。
多项式快速幂
给定 (F(x)) ,求出 (P(x)) 满足 (P(x)equiv F^m(x) pmod{x^n}) 。
两边同时取 (ln) :
先多项式取 (ln) ,再乘上 (m) ,再 (exp) 回去就行了,需要注意的就是在执行 (ln) 操作时要求 (F(x)) 常数项为 (1) ,所以先给 (F(x)) 除上其常数项的值再进行后面操作,最后再乘上其常数项值的 (m) 次方即可还原回去;如果常数项为 (0) ,就先把 (0) 丢掉(整体除以 (x) ),到最后面再加上去(整体乘以 (x^m) )即可。
时间复杂度: (mathcal O(nlog_2n)) 。
多项式多点求值
不会,这个考场上考了就随它去吧。
多项式多点插值
不会,这个考场上考了就随它去吧。
生成函数
形式幂级数
这里可能不太严谨,不过能理解就行了。
对于形式幂级数 (F(x)=sum_{i=0}^{infty}a_ix^i) ,其中我们并不关心 (x) 的数值,且对收敛和发散的问题不感兴趣,只对系数序列 ({a}) 感兴趣,只关心 ({a}) 的值;当数列 ({a}) 有限的时候,也就是存在 (t) ,满足 (a_i(ige t)=0) ,那么 (F(x)) 也可以称为形式多项式。
比如我们就可以认为 (sum_{i=0}^{infty}x^i=frac{1}{1-x}) ,当然这个式子在 (xge 1) 是不成立,但是我们并不关心 (x) 的具体取值,所以就可以默认 (0<x<1) ,然后这样认为。
生成函数就是形式幂级数。
形式幂级数的运算
设 (A(x)=sum_{i=0}^{infty}a_ix^i,B(x)=sum_{i=0}^{infty}b_ix^i) ,有:
乘积就是乘法卷积。
普通型生成函数
序列 ({a}) 的普通型生成函数为:
可以用来解决数列递推等问题。
指数型生成函数
序列 ({a}) 的指数型生成函数为:
可以用来解决组合排列等问题。
题目
为了假装自己学了生成函数和多项式,在这里放两道题目来分析一下。
洛谷P5860 「SWTR-03」Counting Trees
题意简述
给定长度为 (n) 的序列 (v_i) ,询问 ({v}) 中有多少子序列满足分别以子序列中的数作为度数的树存在。
(2le nle 5 imes 10^5,1le v_ile n) 。
题目分析
设选出来了一个长度为 (m) 的子序列 ({a}) ,那么需要满足 (sum_{i=1}^ma_i=2(m-1)) ,即 (sum_{i=1}^m(a_i-2)=-2) ,由此我们可以得出,答案就是 ([x^{-2}]prod_{i=1}^n(x^{v_i-2}+1)) 。
但是我们没有办法处理 (x) 的指数为负的情况,考虑到当 (v_i=1) 时, (x^{v_i-2}+1=frac{1}{x}+1) ,可以乘以 (x) 使得指数变成正数,当然前面的 (x^{-2}) 也要相应地乘以 (x) 。
如何求 (prod_{i=1}^n(x^{t_i}+1)) ?这个和用多项式优化背包时一样的,设 (F_t(x)=x^{t}+1) ,那么要求的就是 (prod_{i=1}^nF_{t_i}(x)=e^{sum_{i=1}^nln(F_{t_i}(x))}) ,于是如果知道了 (ln(F_t(x))) ,就可以先算和,最后只需要一次 (exp()) 就可以还原回去了。
如何算 (ln(F_t(x))=ln(x^t+1)) ?设 (G(x)=ln(x^t+1)) ,那么两边同时求导:
对于每个 (t) 统计 (F_t(x)) 的出现次数然后最后再加起来即可做到 (nlog_2n) 。
需要注意的一点就是当 (v_i=2) 时 , (x^{v_i-2}+1=2) ,需要特殊处理,在 (exp()) 之前先不要乘起来,最后算答案的时候再乘起来。
参考代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ch() getchar()
#define pc(x) putchar(x)
using namespace std;
template<typename T>void read(T&x){
char c;int f;
for(c=ch(),f=1;c<'0'||c>'9';c=ch())if(c=='-')f=-f;
for(x=0;c>='0'&&c<='9';c=ch())x=x*10+(c&15);x*=f;
}
template<typename T>void write(T x){
static char q[64];int cnt=0;
if(x==0)return pc('0'),void();
if(x<0)pc('-'),x=-x;
while(x)q[cnt++]=x%10,x/=10;
while(cnt--)pc(q[cnt]+'0');
}
const int maxn=800005,mod=998244353;
int mo(const int x){
return x>=mod?x-mod:x;
}
int power(int a,int x){
int re=1;
while(x){
if(x&1)re=1ll*re*a%mod;
a=1ll*a*a%mod,x>>=1;
}
return re;
}
namespace polynomial{
int power(int a,int x){
int re=1;
while(x){
if(x&1)re=1ll*re*a%mod;
a=1ll*a*a%mod,x>>=1;
}
return re;
}
const int g_=3;
int rev[maxn];
void initNTT(int m,int&n){
n=1;int cn=-1;
while(n<m)n<<=1,++cn;
for(int i=1;i<n;++i)
rev[i]=(rev[i>>1]>>1)|((i&1)<<cn);
}
void NTT(int*F,int n,int rv){
for(int i=0;i<n;++i)if(rev[i]<i)
F[i]^=F[rev[i]]^=F[i]^=F[rev[i]];
for(int mid=1;mid<n;mid<<=1){
const int len=mid<<1,gn=power(g_,(mod-1)/len);
for(int i=0;i<n;i+=len){
for(int j=0,g=1;j<mid;++j,g=1ll*g*gn%mod){
int l=i+j,r=l+mid;
int L=F[l],R=1ll*F[r]*g%mod;
F[l]=mo(L+R),F[r]=mo(mod-R+L);
}
}
}
if(!rv)return;reverse(F+1,F+n);int I=power(n,mod-2);
for(int i=0;i<n;++i)F[i]=1ll*F[i]*I%mod;
}
int Inv[maxn];
void inv(int*F,int*G,int n){
//G(x)=inv(F(x)) mod x^n;
if(n==1)return G[0]=power(F[0],mod-2),void();
inv(F,G,(n+1)>>1);for(int i=(n+1)>>1;i<n;++i)G[i]=0;
int cp=n;initNTT((n+1)*2,n);
for(int i=0;i<cp;++i)Inv[i]=F[i];for(int i=cp;i<n;++i)Inv[i]=G[i]=0;
NTT(Inv,n,0);NTT(G,n,0);for(int i=0;i<n;++i)
G[i]=1ll*G[i]*mo(mod-1ll*G[i]*Inv[i]%mod+2)%mod;
NTT(G,n,1);for(int i=cp;i<n;++i)G[i]=0;
}
int Ln[maxn];
void ln(int*F,int*G,int n){
//G(x)=ln(F(x)) mod x^n;
for(int i=0;i<n;++i)Ln[i]=0;
inv(F,Ln,n);int cp=n;
initNTT((n+1)*2,n);
for(int i=0;i<cp;++i)G[i]=1ll*F[i+1]*(i+1)%mod;
for(int i=cp;i<n;++i)G[i]=Ln[i]=0;NTT(G,n,0);NTT(Ln,n,0);
for(int i=0;i<n;++i)G[i]=1ll*G[i]*Ln[i]%mod;NTT(G,n,1);
for(int i=cp-1;i>=1;--i)G[i]=1ll*G[i-1]*power(i,mod-2)%mod;
G[0]=0;for(int i=cp;i<n;++i)G[i]=0;
}
int Exp[maxn];
void exp(int*F,int*G,int n){
//G(x)=exp(F(x)) mod x^n;
if(n==1)return G[0]=1,void();exp(F,G,(n+1)>>1);
for(int i=(n+1)>>1;i<n;++i)G[i]=0;
for(int i=0;i<n;++i)Exp[i]=0;ln(G,Exp,n);
for(int i=0;i<n;++i)Exp[i]=mo(mod-Exp[i]+F[i]+(i==0));
int cp=n;initNTT((n+1)*2,n);
for(int i=cp;i<n;++i)Exp[i]=G[i]=0;NTT(G,n,0);NTT(Exp,n,0);
for(int i=0;i<n;++i)G[i]=1ll*G[i]*Exp[i]%mod;
NTT(G,n,1);for(int i=cp;i<n;++i)G[i]=0;
}
int Power[maxn];
void power(int*F,int*G,int n,int m0,int m1,int m2){
//There is something wrong with the code.However, I haven't find out where is the problem.
//G(x)=power(F(x),m) mod x^n;
//m0=m%mod m1=m%(mod-1) m2=stand for m
if(m0==0){for(int i=0;i<n;++i)G[i]=(i==0);return;}
int no=0;while(no<n&&F[no]==0)++no;
if(1ll*no*m2>=n){for(int i=0;i<n;++i)G[i]=0;return;}
int V=F[no],I=power(V,mod-2);
for(int i=0;i+no<n;++i)G[i]=1ll*F[i+no]*I%mod,Power[i]=0;
for(int i=n-no;i<n;++i)G[i]=Power[i]=0;ln(G,Power,n);
for(int i=0;i<n;++i)Power[i]=1ll*Power[i]*m0%mod;
exp(Power,G,n);V=power(V,m1);no*=m2;
for(int i=n-1;i>=no;--i)G[i]=1ll*G[i-no]*V%mod;
for(int i=no-1;i>=0;--i)G[i]=0;
}
int Divide0[maxn],Divide1[maxn];
void divide(int*F,int*G,int*Q,int*R,int n,int m){
//F(x)=Q(x)G(x)+R(x) Q(x)? R(x)? deg(F)=n-1 deg(G)=m-1
int tn=n-m+1;
for(int i=0;i<tn;++i)Divide1[i]=(m>=i+1?G[m-i-1]:0);
inv(Divide1,Divide0,tn);
for(int i=0;i<tn;++i)Divide1[i]=(n>=i+1?F[n-i-1]:0);
int cp=tn;initNTT((n+1)*2,tn);
NTT(Divide0,tn,0);NTT(Divide1,tn,0);
for(int i=0;i<tn;++i)Q[i]=1ll*Divide0[i]*Divide1[i]%mod;
NTT(Q,tn,1);for(int i=cp;i<tn;++i)Q[i]=0;reverse(Q,Q+cp);
for(int i=0;i<tn;++i)Divide0[i]=Q[i],Divide1[i]=G[i];
NTT(Divide0,tn,0);NTT(Divide1,tn,0);
for(int i=0;i<tn;++i)R[i]=1ll*Divide0[i]*Divide1[i]%mod;
NTT(R,tn,1);for(int i=0;i<tn;++i)R[i]=mo(mod-R[i]+F[i]);
}
}
int F[maxn],sum[maxn],G[maxn],inv[maxn];
int main(){
int n,cnt1=0,cnt2=0;read(n);
for(int i=1;i<=n;++i){
int v;read(v);
if(v==1){
++cnt1;
++sum[1];
}
else if(v==2){
++cnt2;
}
else{
++sum[v-2];
}
}
if(cnt1<2)puts("0");
else{
cnt1-=2;
inv[1]=1;
for(int i=2;i<=cnt1;++i)
inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=1;i<=cnt1;++i)
for(int j=1;i*j<=cnt1;++j)
if((j-1)&1)F[i*j]=mo(mod-1ll*sum[i]*inv[j]%mod+F[i*j]);
else F[i*j]=mo(1ll*sum[i]*inv[j]%mod+F[i*j]);
polynomial::exp(F,G,cnt1+1);
write(1ll*G[cnt1]*power(2,cnt2)%mod);pc('
');
}
return 0;
}
/*
所以,永远不要问丧钟为谁而鸣,它为你而鸣。
*/
CF438E The Child and Binary Tree
题意简述
正整数集 (S) 里面的元素分别为 (c_1,c_2,dots,c_n) ,对于每个 (1le ile m) ,询问有多少二叉树满足每个点的点权的值属于 (S) ,并且点权和为 (i) ,两棵二叉树不同当且仅当存在一个点点权不同或者存在一个点的左子树或右子树不同。
(1le n,m,c_ile 10^5) 。
题目分析
设 (f_x) 表示点权和为 (x) 的二叉树个数,默认 (f_0=1) ,那么有:
设 (F(x)=sum_{i=0}^{infty}f_ix^i) ,那么有:
设 (H(x)=sum_{i=1}^nx^{c_i}) ,那么:
套牛顿迭代,可以得到:
参考代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ch() getchar()
#define pc(x) putchar(x)
using namespace std;
template<typename T>void read(T&x){
static char c;static int f;
for(c=ch(),f=1;c<'0'||c>'9';c=ch())if(c=='-')f=-f;
for(x=0;c>='0'&&c<='9';c=ch())x=x*10+(c&15);x*=f;
}
template<typename T>void write(T x){
static char q[65];int cnt=0;
if(x<0)pc('-'),x=-x;
q[++cnt]=x%10,x/=10;
while(x)
q[++cnt]=x%10,x/=10;
while(cnt)pc(q[cnt--]+'0');
}
const int maxn=800005,mod=998244353,g_=3;
int mo(const int x){
return x>=mod?x-mod:x;
}
int power(int a,int x){
int re=1;
while(x){
if(x&1)re=1ll*re*a%mod;
a=1ll*a*a%mod,x>>=1;
}
return re;
}
int rev[maxn];
void initNTT(int m,int&n){
n=1;int cn=-1;
while(n<=m)n<<=1,++cn;
for(int i=0;i<n;++i)
rev[i]=(rev[i>>1]>>1)|((i&1)<<cn);
}
void NTT(int*F,int n,int rv){
for(int i=0;i<n;++i)if(rev[i]<i)
F[rev[i]]^=F[i]^=F[rev[i]]^=F[i];
for(int mid=1;mid<n;mid<<=1){
const int len=mid<<1,gn=power(g_,(mod-1)/len);
for(int i=0;i<n;i+=len){
for(int j=0,g=1;j<mid;++j,g=1ll*g*gn%mod){
int l=i+j,r=l+mid;
int L=F[l],R=1ll*F[r]*g%mod;
F[l]=mo(L+R),F[r]=mo(mod-R+L);
}
}
}
if(!rv)return;reverse(F+1,F+n);int I=power(n,mod-2);
for(int i=0;i<n;++i)F[i]=1ll*F[i]*I%mod;
}
int Inv[maxn];
void inv(const int*F,int*G,int n){
if(n==1)return G[0]=power(F[0],mod-2),void();
int on=(n+1)>>1;inv(F,G,on);for(int i=on;i<n;++i)G[i]=0;
int cp=n;initNTT((n+1)*2,n);for(int i=0;i<cp;++i)Inv[i]=F[i];
for(int i=cp;i<n;++i)Inv[i]=G[i]=0;NTT(Inv,n,0);NTT(G,n,0);
for(int i=0;i<n;++i)G[i]=1ll*G[i]*mo(mod-1ll*G[i]*Inv[i]%mod+2)%mod;
NTT(G,n,1);for(int i=cp;i<n;++i)G[i]=0;
}
int S0[maxn],S1[maxn];
void solve(const int*F,int*G,int n){
if(n==1)return G[0]=1,void();
int on=(n+1)>>1;solve(F,G,on);for(int i=on;i<n;++i)G[i]=0;
int cp=n;initNTT((n+1)*4,n);for(int i=0;i<cp;++i)S0[i]=F[i];
for(int i=cp;i<n;++i)S0[i]=G[i]=0;NTT(G,n,0);NTT(S0,n,0);
for(int i=0;i<n;++i)S0[i]=mo(mod-1+2ll*S0[i]*G[i]%mod);
NTT(S0,n,1);inv(S0,S1,n=cp);initNTT((n+1)*4,n);for(int i=cp;i<n;++i)S1[i]=0;NTT(S1,n,0);
for(int i=0;i<n;++i)G[i]=1ll*(mod+1)/2*mo(G[i]+1ll*mo(mod-2+G[i])*S1[i]%mod)%mod;
NTT(G,n,1);for(int i=cp;i<n;++i)G[i]=0;
}
int F[maxn],G[maxn];
int main(){
int n,m;read(n),read(m);
while(n--){int c;read(c);F[c]=mo(F[c]+1);}
solve(F,G,m+1);for(int i=1;i<=m;++i)write(G[i]),pc('
');
return 0;
}
小结
事实上,自己的多项式和生成函数还是搞得太浅了,没有深入,不过就先入个门吧,以后有时间了再继续搞一下。