多项式-FFT-NTT-数论-学习笔记

i207M

2018-11-20 19:09:38

Personal

**数组下标01搞清楚** 关于 ```cpp for(; fn<=(n<<1); fn<<=1); ``` 那个等号是否加,情况是这样,如果输入n,给你n+1个数,等号必须加;给你n个数,可加可不加。 **在n不同时,r也不同!** **$\pi$的值一定要用double存!** ------ ## FFT 像我这种蒟蒻写一篇介绍FFT的博客有些超出我的能力范围了,就放一个很棒的博客的链接,我就是和他学的。 [FFT-学习笔记](https://www.luogu.org/blog/command-block/fft-xue-xi-bi-ji) 实现? 为了减小常数,使用迭代法。首先我们要找到每个元素最终的位置,其实这就是它二进制表示下的倒序,可以通过**蝴蝶变换**得到。 然后就是大力推式子了,具体的就看代码吧。 ```cpp void dft(cpx f[],int op) { for(ri i=0; i<n; ++i) if(i<r[i]) swap(f[i],f[r[i]]); for(ri p=2; p<=n; p<<=1) { int len=p>>1; cpx nx=cpx(cos(pi/len),op*sin(pi/len)); for(ri k=0; k<n; k+=p) { cpx w=cpx(1,0); for(ri i=k; i<k+len; ++i) { cpx t=w*f[i+len]; f[i+len]=f[i]-t; f[i]=f[i]+t; w=w*nx; } } } } ``` 我们一般写的FFT是处理下标和为定值的情况,即$c[k]=\sum a_ib_{k-i}$,如果要求下标的差为定值的情况$c[k]=\sum a_ib_{i-k}$,只需要把一个数组反过来,这样得到的答案数组再反过来即为所求。 ------ ## NTT 就是模意义下的FFT,可以在已知最大数不会爆模数的情况下,代替FFT的浮点运算来加快。 唯一的变化:单位根是什么?设md的原根为g,则$w=g^{(md-1)/p}$,这要求这个模数要有原根,且$md-1$有很多质因数2; ```cpp void dft(int f[],int op) { for(ri i=0; i<n; ++i) if(i<r[i]) swap(f[i],f[r[i]]); for(ri p=2; p<=n; p<<=1) { int len=p>>1,nx=qpow(op==1?3:332748118,(md-1)/p); for(ri k=0; k<n; k+=p) { int w=1; for(ri i=k; i<k+len; ++i) { int t=(LL)f[i+len]*w%md; f[i+len]=(f[i]-t+md)%md; f[i]=(f[i]+t)%md; w=(LL)w*nx%md; } } } } ``` ------ ## 任意模数NTT-MTT 实质就是NTT+CRT合并。常用的NTT模数为 ```cpp const int md[3]= {469762049,998244353,1004535809}; ``` 对于模板题来说,最大的数字为$10^{18+5=23}$,比long long还要大,所以我们选择三个乘积大于$10^{24}$的质数作为模数即可。 如何合并?这三个质数乘起来也爆long long了(~~高精?~~) 首先用中国剩余定理合并前两个,得到: $\left\{\begin{aligned}x \equiv A \pmod M \\x \equiv a_3 \pmod {m_3} \\\end{aligned}\right.$ 然后设$ans = kM + A$ $k \equiv (a_3 - A)M^{-1} \pmod {m_3}$ 回代求值即可。 注意: 1. CRT合并时,逆元是对于m不是M的。 2. CRT合并要用快速乘。 3. k的值是在模m3意义下的。 ------ **求逆、开根的时候,建议把数组的长度设为2的整次幂,这样不会出现奇怪的问题** ## 多项式求逆 [感谢miskcoo](http://blog.miskcoo.com/2015/05/polynomial-inverse) ![](https://cdn.luogu.com.cn/upload/pic/44189.png) ![](https://cdn.luogu.com.cn/upload/pic/44190.png) 为什么会有无限项?因为多项式中含有未知数,它们相乘怎么也不会不含未知数,所以这个逆元实际上是无限项之和为1,于是我们就要求出$\mod x^n$的结果。 **由这个过程可以看出,一个多项式有没有逆元完全取决于其常数项是否有逆元** ------ ## 多项式除法 ~~舒服!一遍A!~~ [感谢miskcoo](http://blog.miskcoo.com/2015/05/polynomial-division) ![](https://cdn.luogu.com.cn/upload/pic/44191.png) 为了方便,将式子写为$A(x)=B(x)C(x)+D(x)$ 核心思想:通过**系数反转(${A}^R$)**,消除$D(x)$的影响,算出C,回代得到D。 这个值得放一下代码+注释: ```cpp void dft(int n,int f[],int op) { for(ri i=0; i<n; ++i) r[i]=(r[i>>1]>>1)|((i&1)?n>>1:0); for(ri i=0; i<n; ++i) if(i<r[i]) swap(f[i],f[r[i]]); for(ri p=2; p<=n; p<<=1) { int len=p>>1,nx=qpow(op==1?3:332748118,(md-1)/p); for(ri k=0; k<n; k+=p) { int w=1; for(ri i=k; i<k+len; ++i) { int t=(LL)w*f[i+len]%md; f[i+len]=(f[i]-t+md)%md; f[i]=(f[i]+t)%md; w=(LL)w*nx%md; } } } if(op==-1) { int inv=qpow(n,md-2); for(ri i=0; i<n; ++i) f[i]=(LL)f[i]*inv%md; } } void inverse(int n,int a[],int b[]) { if(n==1) { b[0]=qpow(a[0],md-2); return; } inverse((n+1)>>1,a,b); // 递归 int fn=1; for(; fn<(n<<1); fn<<=1); for(ri i=0; i<n; ++i) tmpc[i]=a[i]; for(ri i=n; i<fn; ++i) tmpc[i]=0; // 记得将高位清零 dft(fn,tmpc,1), dft(fn,b,1); for(ri i=0; i<fn; ++i) b[i]=b[i]*(2-(LL)tmpc[i]*b[i]%md+md)%md; dft(fn,b,-1); for(ri i=n; i<fn; ++i) b[i]=0; // 记得将高位清零 } void divi(int n,int a[],int m,int b[],int c[],int d[]) { int t=n-m+1,fn=1; for(; fn<(t<<1); fn<<=1); for(ri i=0; i<t; ++i) tmpa[i]=a[n-i],tmpb[i]=b[m-i]; // 反转后存入数组 inverse(t,tmpb,c); dft(fn,tmpa,1), dft(fn,c,1); for(ri i=0; i<fn; ++i) c[i]=(LL)c[i]*tmpa[i]%md; // 乘逆元 dft(fn,c,-1); reverse(c,c+t); for(ri i=t; i<fn; ++i) c[i]=0; // 得到C后高位清零 for(fn=1; fn<(m<<1); fn<<=1); for(ri i=0; i<fn; ++i) tmpa[i]=tmpb[i]=0; for(ri i=0; i<m; ++i) tmpb[i]=b[i],tmpa[i]=c[i]; dft(fn,tmpb,1), dft(fn,tmpa,1); for(ri i=0; i<fn; ++i) tmpa[i]=(LL)tmpa[i]*tmpb[i]%md; // 计算 B*C dft(fn,tmpa,-1); for(ri i=0; i<m; ++i) d[i]=(a[i]-tmpa[i]+md)%md; } ``` ------ ~~多项式开根、指数、对数:未完待续~~ ## 多项式开根 ![](https://cdn.luogu.com.cn/upload/pic/44208.png) ~~手推出来了好开心~~ 据说常数是DFT的14倍... ```cpp void Sqrt(int n,int a[],int b[]) { if(n==1) { b[0]=1; return; } Sqrt(n>>1,a,b); inv(n,b,tmpb); int m=pre(n<<1); for(ri i=0; i<n; ++i) tmpa[i]=a[i]; dft(tmpa,m,1); dft(tmpb,m,1); dft(b,m,1); for(ri i=0; i<m; ++i) b[i]=((LL)tmpa[i]*tmpb[i]%md+b[i])%md*inv2%md; dft(b,m,-1); for(ri i=n; i<m; ++i) b[i]=0; for(ri i=0; i<m; ++i) tmpa[i]=tmpb[i]=0; } ``` ------------------ ~~本来不打算去学对数的,但是看起来很简单就学了~~ ## 多项式对数 $lnA(x)=\int (lnA(x))'=\int\frac{A'(x)}{A(x)}$ 因为多项式很容易求导、积分,所以这种方法比较妙。 $b_i=(i+1)a_{i+1}$ $c_i=\frac {a_{i-1}}i$ 关于取对数时要保证$a_0=1$的原因:当x=0时,$\ln A(0)=\ln 1=0$,我们求出的答案B(x)恰好常数项为0,x=0时$B(x)=0$,符合题意。 否则不定积分的常数项$C=\ln a_0 \mod p$,在模意义下取自然对数...反正我不会。 ~~指数需要牛顿迭代法,先缓缓~~ ------ ## cdq分治FFT $f[i]=\sum_{j=1}^if[i-j]g[j]$ f这个多项式没有完全给你,只能一边算一边求。像这种问题,可以使用——cdq分治! 每次统计左区间对右区间的贡献。 把$f[l...mid]$拿出来,放到$a[0...(mid-l)]$里。 把$g[0...r-l]$拿出来,放到$b$。 把它们卷起来,结果放到$a$。 把$a[(mid-l+1)...(r-l)]$对应加到$f[(mid+1)...r]$上。 ```cpp int a[N*4],b[N*4]; int f[N],g[N]; void fft(int l,int mid,int R) { int len=R-l+1,m=1; for(ri i=l; i<=mid; ++i) a[i-l]=f[i]; for(ri i=0; i<len; ++i) b[i]=g[i]; len+=mid-l+1; for(; m<=len; m<<=1); for(ri i=1,hb=m>>1; i<m; ++i) r[i]=(r[i>>1]>>1)|((i&1)?hb:0); dft(a,m,1), dft(b,m,1); for(ri i=0; i<m; ++i) a[i]=(LL)a[i]*b[i]%md; dft(a,m,-1); for(ri i=mid+1; i<=R; ++i) (f[i]+=a[i-l])%=md; for(ri i=0; i<m; ++i) a[i]=b[i]=0; } void cdq(int l,int r) { if(l>=r) return; int mid=(l+r)/2; cdq(l,mid); fft(l,mid,r); cdq(mid+1,r); } ``` ------------------ ## 多项式快速幂 求一个次数为n的多项式的k次方,保证$nk<=10^6$ 当然可以使用传说中的(真·)分治FFT来搞,复杂度$O(N\log N),N=nk$。 但是我们还有更加优美的方法: 直接把原多项式dft成点值表达,然后把每个位置变成$k$次方,再idft回去即可。记得要把范围扩大。 求多项式快速幂模另一个多项式的结果,就想快速幂一样,步步取模即可。写起来挺恶心。 ------ ## 多项式牛顿迭代 ### 牛顿迭代 ![bd3df86a7c7dd74f13ef459de99df5652b393f6b_6.jpg](https://i.loli.net/2019/01/28/5c4ee15f5665c.jpg) 牛顿迭代的精度每迭代一次,精度可以近似看作翻倍。 *进入正题*。 我们已知多项式$G(x)$,要求出$F(x)$使得$G(F(x))=0 \mod x^n$。 ![捕获.PNG](https://i.loli.net/2019/01/28/5c4ee26d3a212.png) 好,接下来我们实际应用多项式牛顿迭代来解决多项式开方: ![捕获.PNG](https://i.loli.net/2019/01/28/5c4ee30272e2e.png) 有一个疑问:为什么在求导的时候,忽略了$A(x)$?原因是我们求导的时候,只把$F(x)$看成$x$,不含$F(x)$的想都看成常数。 你问我为什么不用多项式牛顿迭代解决多项式求逆?因为它需要多项式求逆啊! **注意:** 多项式牛顿迭代不一定能完美拟合所有函数! ---------------- ## 多项式指数函数 ![](https://cdn.luogu.com.cn/upload/pic/50148.png) $\exp$常数项是1 ```cpp void Inv(int n,int a[],int b[]) { if(n==1) { b[0]=1; return; } Inv(n>>1,a,b); static int c[N]; for(ri i=0; i<n; ++i) c[i]=a[i]; int m=pre(n<<1); dft(c,m,1); dft(b,m,1); for(ri i=0; i<m; ++i) b[i]=(2-(LL)b[i]*c[i]%md+md)*b[i]%md; dft(b,m,-1); for(ri i=n; i<m; ++i) b[i]=0; for(ri i=0; i<m; ++i) c[i]=0; } int iv[N]; void Ln(int n,int a[],int b[]) { static int c[N]; for(ri i=1; i<n; ++i) b[i-1]=(LL)a[i]*i%md; Inv(n,a,c); int m=pre(n<<1); dft(b,m,1); dft(c,m,1); for(ri i=0; i<m; ++i) b[i]=(LL)b[i]*c[i]%md; dft(b,m,-1); for(ri i=n-1; i>=1; --i) b[i]=(LL)b[i-1]*iv[i]%md; b[0]=0; for(ri i=n; i<m; ++i) b[i]=0; for(ri i=0; i<m; ++i) c[i]=0; } void Exp(int n,int a[],int b[]) { if(n==1) { b[0]=1; return; } Exp(n>>1,a,b); static int c[N]; Ln(n,b,c); for(ri i=0; i<n; ++i) c[i]=(a[i]-c[i]+md)%md; (++c[0])%=md; int m=pre(n<<1); dft(b,m,1); dft(c,m,1); for(ri i=0; i<m; ++i) b[i]=(LL)b[i]*c[i]%md; dft(b,m,-1); for(ri i=n; i<m; ++i) b[i]=0; for(ri i=0; i<m; ++i) c[i]=0; } int n; int a[N],b[N]; signed main() { #ifdef M207 freopen("in.in","r",stdin); // freopen("out.out","w",stdout); #endif in(n); for(ri i=0; i<n; ++i) in(a[i]); int m=1; for(; m<n; m<<=1); for(ri i=1; i<m; ++i) iv[i]=qpow(i,md-2); Exp(m,a,b); print(b,n); return 0; } ``` **通过ln和exp解决最低次项不为1的多项式快速幂:除以最低次项** 多项式$\ln,\exp$主要是用来解决排列问题(多重集合的排列),对于图的问题,可用来切换联通与不联通。eg.设$g$为n个点的无向图的个数,$f$为n个点的无向连通图的个数,则$g=\exp f,f=\ln g$ 例题:城市规划 我们来证明一下上个式子(by Mr_Spade): 可以发现一个无向图就是由若干个无向连通图组成的,因此我们可以枚举组成这个无向图的连通块个数计算贡献。假设我们当前枚举的是$k$个,它的贡献就为: $\large \frac{f^k}{k!}$ $f^k$很好理解,为什么要除以$k!$呢?因为我们计算的$g_i$是要除以$i!$的,相当于要消除标号的影响,而$f^k$所乘的$k$个元素是有先后顺序之分的,因此需要除以全排列的数目$k!$。那么$f$和$g$的关系式就为: $\large g=\sum \limits_{k=0}^{\infty}\frac{f^k}{k!}$ 这个式子是不是有点眼熟呢?没错!右边那项就是$e^f$的麦克劳林级数。 其实我觉得这个题解的解释就挺好,我再解释一下。首先,指数生成函数相乘,系数就是多重集合的排列。而$f^k$就是把n拆成k个集合的情况。但是我们的每个多项式是不同的,所以每一项都多算了$k!$次。 如果你还是不理解,可以尝试一种暴力理解方式: ~~考虑一个整数划分会被多项式乘出来多少次?是**多重集合排列**次,设我们关注第n项,即为$\frac{n!}{a!b!...}$,好,我们先无脑除以前面的式子,现在的系数是$\frac{a!b!...}{n!}$,但是,这样就去重完了吗?考虑$4=2+2$这种拆分,我们在多重集合排列时认为这两个2是不同的,但其实它们是相同的,所以要除以$2!$。回到原来的式子,我们要除以$a!b!...$,鼓捣一番后发现,真的只剩下分母的$n!$了。~~ 不要看上面的暴力解释了,我也不知道我在说啥。 我们设f是不一定联通的方案数,g是联通的方案数,那么有 $$f(n)=\sum_{i=1}^n {n-1 \choose i-1}f(n-i)g(i)$$ 移项之后为 $$\frac{f(n)}{(n-1)!}=\sum_{i=1}^n \frac{g(i)}{(i-1)!}\frac{f(n-i)}{(n-i)!}$$ 也就是 $$F'=G'F$$ ~~接下来开始伪证~~ 若$F=e^G$,则$F'=G'F$ 所以得证。 ---------------- ## 多项式三角函数 目前来看没啥用,所以先mark着。[by Semiwaker](https://blog.csdn.net/semiwaker/article/details/73251486) 求$sin A(x),cos A(x)$ ![img](https://cdn.luogu.com.cn/upload/pic/51042.png) 对于NTT也可以的![Link](https://www.luogu.org/blog/user7035/duo-xiang-shi-zong-jie) [应用](https://en.wikipedia.org/wiki/Alternating_permutation) --------------------- ## 多项式多点求值 ~~hu~调了半天发现竟然是对负数取模出错了...~~ 我们的老朋友,分治。我们考虑将问题分治成两个小问题。我们就简单粗暴的把询问分成前一半和后一半,如果这个时候,多项式的系数也能减半,就好了。我们构造多项式 $\begin{eqnarray*} P^{[0]}(x) &=& \prod_{i=0}^{\lfloor \frac{n}{2} \rfloor} (x-x_i) \end{eqnarray*}$ 这样的多项式,在我们要求的地方值为0,所以不会影响最终答案。我们可以任意减去它——也就是多项式取模! ```cpp const int rt=1; #define ls (x<<1) #define rs (x<<1|1) #define gm int mid((l+r)/2) int BUK[N*50],*buk=BUK; int f[N],a[N]; int *pol[N]; int *init(int sz) { int *res=buk; buk+=sz; return res; } void build(int x,int l,int r) { if(l==r) { pol[x]=init(2); pol[x][0]=md-a[l],pol[x][1]=1; return; } gm; build(ls,l,mid); build(rs,mid+1,r); pol[x]=init(r-l+2); Mul(mid-l+2,pol[ls],pol[rs],pol[x]); } void del(int sz) { while(sz--) *--buk=0; } int ans[N]; void solve(int x,int l,int r,int *fuc) { if(r-l<=100) { for(ri i=l; i<=r; ++i) { int &s=ans[i]; for(ri j=r-l; j>=0; --j) s=((LL)s*a[i]+fuc[j])%md; } return; } gm; int *p=init(mid-l+1); Div(r-l+1,fuc,mid-l+2,pol[ls],p); solve(ls,l,mid,p); Div(r-l+1,fuc,r-mid+1,pol[rs],p); solve(rs,mid+1,r,p); del(r-mid); } int n,m; signed main() { #ifdef M207 freopen("in.in","r",stdin); // freopen("out.out","w",stdout); #endif in(n,m); ++n; for(ri i=0; i<n; ++i) in(f[i]); for(ri i=1; i<=m; ++i) in(a[i]); int m2=1; for(; m2<m; m2<<=1); swap(m,m2); build(rt,1,m); if(n>m) Div(n,f,m+1,pol[rt],f); solve(rt,1,m,f); for(ri i=1; i<=m2; ++i) out(ans[i]); return 0; } ``` ---------------------- ## 多项式快速插值 [by xyz32768](https://blog.csdn.net/xyz32768/article/details/82832467) ![捕获.PNG](https://i.loli.net/2019/02/07/5c5b8ecd68e22.png) ![捕获.PNG](https://i.loli.net/2019/02/07/5c5b8eeaec83a.png) ```cpp int n; int X[N],Y[N]; int *tf[N]; void solve(int x,int l,int r) { if(l==r) { tf[x]=init(1); tf[x][0]=(LL)Y[l]*qpow(gv[l],md-2)%md; return; } gm; solve(ls,l,mid); solve(rs,mid+1,r); tf[x]=init(r-l+1); static int tmp[N]; int t=Mul(mid-l+1,tf[ls],r-mid+1,pol[rs],tmp); for(ri i=0; i<t; ++i) tf[x][i]=tmp[i],tmp[i]=0; t=Mul(r-mid,tf[rs],mid-l+2,pol[ls],tmp); for(ri i=0; i<t; ++i) (tf[x][i]+=tmp[i])%=md,tmp[i]=0; } int dg[N]; signed main() { #ifdef M207 freopen("in.in","r",stdin); // freopen("out.out","w",stdout); #endif in(n); for(ri i=1; i<=n; ++i) { in(X[i],Y[i]); a[i]=X[i]; } build(rt,1,n); for(ri i=1; i<=n; ++i) dg[i-1]=(LL)pol[rt][i]*i%md; calc(rt,1,n,dg); solve(rt,1,n); print(tf[rt],n); return 0; } ``` ~~实用的~~多项式科技基本完结了吧...