门的另一端

可爱得很有力量的歌,丘丘人的部分感觉好听炸了。

偶然推开的那一扇门

从何时起变得不再陌生

对面有人 可爱地等

在遇见你的这个世界

时针似乎转得要比平时快些

天空 更清澈了一些

相遇的理由

幻化成了歌

揉进了旅途的景色

为什么此刻

心脏在愉快地蹦着

明明触摸不到的风

却偷偷把汗水吹走

闻不到的饭香

却让我更坚强 更勇敢

明明是受不到的伤

心却还是隐隐作痛鼻腔发痒

明明那是我未曾踏足的彼方

这个地方有一点奇怪

炊烟不会断 花无尽地开

人比猪懒 最爱使唤

偶尔对着天空发个呆

直到云朵透出日落果的色彩

你看 是不是很像

我这个品种

总聚少离多

寂寞能写十本小说

从来没有过

有过这么多

Ye tomo

明明触摸不到的风

却偷偷把汗水吹走

闻不到的饭香

却让我更坚强 更勇敢

明明是受不到的伤

心却还是隐隐作痛鼻腔发痒

想再靠近一些 这熟悉的温暖

没完没了的话

每次听到还是 很喜欢

一首歌太短

在门的另一端 故事还没写完

Yeye dada

Mimi tomo

Mosi mita

Gusha gusha nye

Gusha gusha nye nye

温馨提示

本文更注重讲清 DFT、IDFT 等的实现原理,给出的代码跑得不快且未经良好的封装。

如果您渴望找到一份好用的多项式板子,请移步其它博客。

定义

多项式是一个形如 \(f(x)=\sum_{i}{a_i x^i}\) 的式子,我们也会用它的系数表示它,如表示为 \(<a_0,a_1,\cdots,a_{n-1}>\)。

如无特殊说明,以下 \(n\) 均为多项式的项数。如对于多项式 \(f\),\(\deg f=n-1\)。

以下默认讨论的是多项式 \(a,b\) 相乘得 \(c\),其中 \(a,b\) 中长度较大的一个长度为 \(n\),我们用下标 \(a_i\) 表示 \(i\) 次项的系数,用括号 \(a(x)\) 表示将 \(x\) 代入 \(a\) 求得的值。

我们用 \(\omega_{a}\) 表示 \(a\) 次单位根中辐角最小的一个,也即 \(\cos(\frac{2\pi}{a})+\sin(\frac{2\pi}{a})i\),\(\omega^{b}_{a}\) 就是它的 \(b\) 次方。容易发现 \(\omega^{a}_{a}=\omega^{0}_{a}=1\)。

引入

当我们要将两个多项式相乘时,即计算 \(c_i=\sum_{j+k=i}{a_j b_k}\),暴力计算的复杂度为 \(O(n^2)\) 的。考虑如果两个多项式都是点值表示的,那我们容易得到 \(c(x)=a(x)b(x)\),毕竟对于 \(a(x)b(x)\) 的第 \(i\) 项,应为

\[\begin{aligned}
&\sum_{j+k=i}{a_j x^j b_k x^k}\\=&\sum_{j+k=i}{a_j b_k x^i}\\=&c_i x^i
\end{aligned}
\]

所以可以快速地得到 \(c\) 的点值表示。接下来我们尝试建立点值表示与系数间的关系。

DFT 与 IDFT

我们先来考虑一个性质:

\[\frac{1}{n}\sum_{i=0}^{n-1}\omega_{n}^{ai}=[a \equiv 0 \bmod n]
\]

当 \([a \equiv 0 \bmod n]\) 时,每一个 \(\omega_{n}^{ai}\) 都是 \(1\),这个式子的值也为 \(1\)。否则,这个式子可以变成等比数列求和的形式,即

\[\begin{aligned}
&\frac{1}{n}\sum_{i=0}^{n-1}\omega_{n}^{ai}\\=&\frac{\omega_{n}^{a(n-1)}\omega_{n}^{a}-\omega_{n}^{0}}{\omega_{n}^{a}-1}\\=&\frac{1-1}{\omega_{n}^{a}-1}\\=&0
\end{aligned}
\]

接下来回到原式,我们考虑

\[\begin{aligned}
&c_i\\=&\sum_{j+k=i}a_j b_k\\=&\sum_{j,k}[j+k=i]a_j a_k
\end{aligned}
\]

我们先考虑另一个和它长得很像的式子:

\[\begin{aligned}
&\sum_{j,k}[j+k \equiv i \bmod n]a_j a_k\\=&\sum_{j,k}[j+k-i \equiv 0 \bmod n]a_j a_k\\=&\sum_{j,k}\frac{1}{n}\sum_{l=0}^{n-1}\omega_{n}^{l(j+k-i)}a_j b_k\\=&\frac{1}{n}\sum_{j,k}\sum_{l=0}^{n-1}\omega_{n}^{-il}\omega_{n}^{jl}a_j\omega_{n}^{kl}k_l\\=&\frac{1}{n}\sum_{l=0}^{n-1}\omega_{n}^{-il}\sum_{j=0}^{n-1}\omega_{n}^{jl}a_j\sum_{k=0}^{n-1}\omega_{n}^{kl}b_k
\end{aligned}
\]

其中第二步代入了上面的结论。

我们发现这个最终的形式非常优美,因为它可以分成几个相似的部分。我们先来尝试建立原式和该式之间的联系。考虑他们的区别是把 \([j+k=i]\) 变成了 \([j+k \equiv i \bmod n]\),这样做可能使得 \(j+k=i+n\) 的 \(j,k\) 被多算进 \(c_i\) 的贡献。但最终式子 \(c\) 的长度一定是小于 \(2n\) 的,如果我们考虑将两个原式的长度都按 \(2n\) 考虑,就能避免上面的情况,而且其它情况如 \(j+k=i+2n\) 是没有意义的,因为这样 \(j,k\) 必有一个大于等于 \(n\),此时 \(a_j b_k=0\)。这样,我们就可以通过求下式得到上式的答案。

我们发现 \(d_i=\sum_{j=0}^{n-1}\omega_{n}^{ij}a_j\) 和 \(e_i=\sum_{j=0}^{n-1}\omega_{n}^{ij}b_j\),就是将 \(\omega_{n}^{i}\) 带入 \(a,b\) 的点值,而 \(c_i=\sum_{j=0}^{n-1}\omega_{n}^{-ij}d_j e_j\) 则建立了一种点值到系数的关系。这两中变换便分别是 DFT 和 IDFT 的一种体现。接下来我们考虑怎么快速计算他们。

FFT

我们先只考虑 DFT,也就是求点值的过程。考虑多项式

\[\begin{aligned}
&f(x)\\=&a_0 +a_1 x+a_2 x^2+a_3 x^3\\=&a_0+a_2 x^2+x(a_1 +a_3 x^2)
\end{aligned}
\]

这样将奇次与偶次分离之后,假设 \(g(x)\) 为 \(<a_0,a_2>\),\(h(x)\) 为 \(<a_1,a_3>\),那么

\[f(x)=g(x^2)+x h(x^2)
\]

假设 \(x=\omega_{n}^{i}\),那么

\[\begin{aligned}
&f(\omega_{n}^{i})\\=&g(\omega_{n}^{2i})+\omega_{n}^{i}h(\omega_{n}^{2i})\\=&g(\omega_{\frac{n}{2}}^{i})+\omega_{n}^{i}h(\omega_{\frac{n}{2}}^{i})
\end{aligned}
\]

假设 \(n\) 为 \(2\) 的平方数,这样我们把长度为 \(n\) 的式子递归为 \(O(\log n)\) 层处理,每层都是若干个长度为 \(a=2^k\) 的多项式,需要分别带入 \(a\) 个值(\(\omega_{a}^{0}\) 到 \(\omega_{a}^{a-1}\))计算,总复杂度为 \(O(n \log n)\)。

尽管复杂度已经够优秀了,但是这种递归的实现非常不优美。我们假设存储结果的序列为 \(p\),原序列为 \(a\),我们先考虑我们是怎么归类系数奇偶的。先把偶数提前,奇数靠后,再将第二个二进制位是 \(0\) 的提前……可以发现这样做相当与将系数按照原位置排序,第 \(i\) 个二进制位为 \(i\) 关键字,可以发现每个数的最终位置就是将它二进制位反转后得到的位置,这样得到的序列在上述排序标准下不存在逆序对。假设 \(i\) 对应的位置为 \(r_i\),那么我们可以先令 \(p_{r_i}=a_{i}\),然后我们发现每个上级式子的两个下级式子的系数正好在它的前半部分和后半部分,因为我们对上一关键字排序后对下一关键字的排序是在这个固定的区间内进行的。考虑一开始 \(p_i\) 就是 \(<p_i>\) 代入 \(\omega_{1}^{0}\) 的解,之后每一次运算我们都把这个这个式子代入 \(i\) 次本源单位根的点值存在这个区域的第 \(i\) 个位置,合并到上级式子时发现代入 \(i\) 次本源单位根和 \(i+\frac{n}{2}\) 次本源单位根所需数据的位置是一样的,也就是将要存储这两个数据的位置,因此可以同时处理。最终我们用 \(O(n)\) 的空间倍增解决了这个问题。

对于 IDFT,我们发现 \(\omega_{n}^{-i}=\omega_{n}^{n-i}\),假设我们像处理 DFT 一样依次代入 \(0,1,\cdots,n-1\) 次本源单位根,发现除了 \(\omega_{n}^{0}\) 代入结果就是 \(c_0\) 以外 \(\omega_{n}^{i}\) 代入结果其实是 \(c_{n-i}\),所以我们只需按 DFT 处理再除以 \(n\) 并将后 \(n-1\) 项反转即可。

给出一份并不很快的 P3803 【模板】多项式乘法(FFT)通过代码。

代码
#include<bits/stdc++.h>
using namespace std;
const double pi=acos(-1.0);
struct cplx{
double rl,im;
friend cplx operator + (cplx x,cplx y){
return cplx{x.rl+y.rl,x.im+y.im};
}
void operator += (cplx x){
rl+=x.rl,im+=x.im;
}
friend cplx operator - (cplx x,cplx y){
return cplx{x.rl-y.rl,x.im-y.im};
}
friend cplx operator * (cplx x,cplx y){
return cplx{x.rl*y.rl-x.im*y.im,x.rl*y.im+x.im*y.rl};
}
void operator *= (cplx x){
double tem=rl;
rl=rl*x.rl-im*x.im,im=tem*x.im+x.rl*im;
}
}w[40];
int r[2200100];
inline void fft(bool opt,int len,cplx* x){
for(int i=0;i<len;++i) if(r[i]>i) swap(x[i],x[r[i]]);
for(int l=2,k=1;l<=len;l<<=1,++k){
cplx tem=cplx{1,0};
for(int i=0;i<len;++i){
if(i&(l-1)&(l>>1)){
i+=(l>>1)-1,tem=cplx{1,0};
continue;
}
cplx tt1=x[i],tt2=tem*x[i|(l>>1)];
x[i]+=tt2;
x[i|(l>>1)]=tt1-tt2;
tem*=w[k];
}
}
if(opt){
for(int i=0;i<len;++i) x[i].rl/=len;
reverse(x+1,x+len);
}
}
struct poly{
int len;
cplx data[2200100];
inline void read(int x){
for(int i=0;i<x;++i){
if(data[i].rl>=-0.5&data[i].rl<0) printf("0 ");
else printf("%.0f ",data[i].rl);
}
}
friend poly operator * (poly x,poly y){
int tlen=1;
poly rtr;
for(;tlen<(max(x.len,y.len)<<1);tlen<<=1);
for(int i=1;i<tlen;++i){
r[i]=r[i>>1]>>1;
if(i&1) r[i]|=(tlen>>1);
}
fft(0,tlen,x.data),fft(0,tlen,y.data);
for(int i=0;i<tlen;++i) rtr.data[i]=x.data[i]*y.data[i];
fft(1,tlen,rtr.data);
for(rtr.len=tlen;rtr.data[rtr.len-1].rl==0;--rtr.len);
return rtr;
}
}a,b,c;
int main(){
for(int i=1,j=1;i<24;++i,j<<=1){
w[i]=cplx{cos(pi/j),sin(pi/j)};
}
scanf("%d%d",&a.len,&b.len);
int tl=a.len+b.len+1;
++a.len;
for(int i=0;i<a.len;++i) scanf("%lf",&a.data[i].rl);
++b.len;
for(int i=0;i<b.len;++i) scanf("%lf",&b.data[i].rl);
c=a*b,c.read(tl);
return 0;
}

NTT

考虑计算 \(c_i\) 在模 \(p\) 意义下的结果。我们考虑对于奇素数 \(p\),如果它有原根 \(g\),那么

\[\omega_{n} \equiv g^{\frac{\varphi(p)}{n}} \bmod p
\]

其中一个数的原根即为使得 \(b=\varphi(p)\) 是使 \(g^b \equiv 1 \bmod p\) 最小的 \(b\) 的 \(g\)。可以发现 \(g^0,g^1,\cdots,g^{\varphi(p)-1}\) 在模 \(p\) 意义下互不相等,否则对于 \(g^a=g^b\),\(g^{a-b} \equiv 1 \bmod p\) 与 \(\varphi(p)\) 最小性矛盾。这样我们保证了这 \(n\) 个单位根互不相同,其余按照 FFT 进行即可。

注意 NTT 模数大都形如 \(k2^a+1\),其中 \(a\) 足够大保证了 \(g^{\frac{\varphi(p)}{n}}\) 运算的正常进行(因为 \(n\) 是 \(2\) 的次方数)。常见的 NTT 模数 \(998244353=7\times 17 \times 2^{23} +1\),它的一个原根为 \(3\)。

给出一份 P3803 【模板】多项式乘法(FFT)的通过代码。因为保证了各项系数小于 \(10\),所以可以按模 \(998244353\) 意义下的 NTT 做。

代码
#include<bits/stdc++.h>
using namespace std;
const long long p=998244353;
int r[2200100];
inline long long qpow(long long x,long long y){
long long rtr=1;
for(;y;y>>=1){
if(y&1) rtr=rtr*x%p;
x=x*x%p;
}
return rtr;
}
inline void ntt(bool opt,int len,long long *x){
for(int i=0;i<len;++i) if(r[i]>i) swap(x[i],x[r[i]]);
for(int l=2,k=1;l<=len;l<<=1,++k){
long long tt=qpow(3,(p-1)>>k),tem=1;
for(int i=0;i<len;++i){
if(i&(l-1)&(l>>1)){
i+=(l>>1)-1,tem=1;
continue;
}
long long tt1=x[i],tt2=tem*x[i|(l>>1)]%p;
x[i]+=tt2; if(x[i]>=p) x[i]-=p;
x[i|(l>>1)]=tt1-tt2; if(x[i|(l>>1)]<0) x[i|(l>>1)]+=p;
tem=tem*tt%p;
}
}
if(opt){
long long tem=qpow(len,p-2);
for(int i=0;i<len;++i) x[i]=x[i]*tem%p;
reverse(x+1,x+len);
}
}
struct poly{
int len;
long long data[2200100];
inline void read(int x){
for(int i=0;i<x;++i) if(data[i]<0) data[i]+=p;
for(int i=0;i<x;++i) printf("%lld ",data[i]);
printf("\n");
}
friend poly operator * (poly x,poly y){
int tlen=1;
poly rtr;
for(;tlen<(max(x.len,y.len)<<1);tlen<<=1);
for(int i=1;i<tlen;++i){
r[i]=r[i>>1]>>1;
if(i&1) r[i]+=tlen>>1;
}
ntt(0,tlen,x.data),ntt(0,tlen,y.data);
for(int i=0;i<tlen;++i) rtr.data[i]=x.data[i]*y.data[i]%p;
ntt(1,tlen,rtr.data);
for(rtr.len=tlen;!rtr.data[rtr.len-1];--rtr.len);
return rtr;
}
}a,b,c;
int main(){
scanf("%d%d",&a.len,&b.len);
int tl=a.len+b.len+1;
++a.len;
for(int i=0;i<a.len;++i) scanf("%lld",&a.data[i]);
++b.len;
for(int i=0;i<b.len;++i) scanf("%lld",&b.data[i]);
c=a*b,c.read(tl);
return 0;
}

Acknowledgement

感谢 wang54321 掀起机房学习多项式的大潮。

感谢 xrlong 把我带出了学习一大堆根本没有什么关系的前置知识的歧路。

最后感谢傅里叶大神。

Reference

门的另一端 - 百度百科

原根 - OI Wiki

快速傅里叶变换 - OI Wiki

快速数论变换 - OI Wiki

[学习笔记&教程] 信号, 集合, 多项式, 以及各种卷积性变换 (FFT,NTT,FWT,FMT) - rvalue - 博客园

FFT+NTT入门 - CuFeO4 - 博客园

【闲话 No.5】 FFT 与 NTT 基础的更多相关文章

  1. FFT和NTT学习笔记_基础

    FFT和NTT学习笔记 算法导论 参考(贺) http://picks.logdown.com/posts/177631-fast-fourier-transform https://blog.csd ...

  2. 多项式fft、ntt、fwt 总结

    做了四五天的专题,但是并没有刷下多少题.可能一开始就对多项式这块十分困扰,很多细节理解不深. 最简单的形式就是直接两个多项式相乘,也就是多项式卷积,式子是$N^2$的.多项式算法的过程就是把卷积做一种 ...

  3. 多项式乘法,FFT与NTT

    多项式: 多项式?不会 多项式加法: 同类项系数相加: 多项式乘法: A*B=C $A=a_0x^0+a_1x^1+a_2x^2+...+a_ix^i+...+a_{n-1}x^{n-1}$ $B=b ...

  4. fft,ntt总结

    一个套路:把式子推成卷积形式,然后用fft或ntt优化求解过程. fft的扩展性不强,不可以在fft函数里多加骚操作--DeepinC T1:多项式乘法 板子题 T2:快速傅立叶之二 另一个板子,小技 ...

  5. FFT与NTT专题

    先不管旋转操作,考虑化简这个差异值 $$begin{aligned}sum_{i=1}^n(x_i-y_i-c)^2&=sum_{i=1}^n(x_i-y_i)^2+nc^2-2csum_{i ...

  6. 【基础操作】FFT / DWT / NTT / FWT 详解

    1. 2. 点值表示法 假设两个多项式相乘后得到的多项式 的次数(最高次项的幂数)为 $n$.(这个很好求,两个多项式的最高次项的幂数相加就得到了) 对于每个点,要用 $O(n)$ 的时间 把 $x$ ...

  7. FFT/NTT基础题总结

    在学各种数各种反演之前把以前做的$FFT$/$NTT$的题整理一遍 还请数论$dalao$口下留情 T1快速傅立叶之二 题目中要求求出 $c_k=\sum\limits_{i=k}^{n-1}a_i* ...

  8. 「学习笔记」FFT及NTT入门知识

    前言 快速傅里叶变换(\(\text{Fast Fourier Transform,FFT}\) )是一种能在\(O(n \log n)\)的时间内完成多项式乘法的算法,在\(OI\)中的应用很多,是 ...

  9. 卷积FFT、NTT、FWT

    先简短几句话说说FFT.... 多项式可用系数和点值表示,n个点可确定一个次数小于n的多项式. 多项式乘积为 f(x)*g(x),显然若已知f(x), g(x)的点值,O(n)可求得多项式乘积的点值. ...

  10. 多项式的基本运算(FFT和NTT)总结

    设参与运算的多项式最高次数是n,那么多项式的加法,减法显然可以在O(n)时间内计算. 所以我们关心的是两个多项式的乘积.朴素的方法需要O(n^2)时间,并不够优秀. 考虑优化. 多项式乘积 方案一:分 ...

随机推荐

  1. ZeroTier简单使用

    在 CentOS 系统下,你可以使用以下命令行操作来管理 ZeroTier 网络和设备.首先,确保已经正确安装 ZeroTier 软件,你可以按照以下步骤进行安装: 安装 ZeroTier: Zero ...

  2. explorer

    explorer 是 Windows 下的一个实用命令. 实例 打开文件浏览器 explorer # 效果等同于快捷键操作 [Win + E] 使用默认浏览器打开链接 explorer "h ...

  3. AspNetCore MVC 跨域

    通过XMLHttpRequest或者ajax去请求一个AspNetCore API接口服务时,Firefox提示我 已拦截跨源请求:同源策略禁止读取位于 http://localhost:33694/ ...

  4. python练习-爬虫

    场景: 1.网址hppt://xxx.yyy.zzz.cn2.打开网页后显示 : 3.填上姓名 身份证和验证码,点击查询后,返回查询结果. 4.页面有cookie. 方案一: 程序中嵌入浏览器根据网址 ...

  5. Greenplum数据库时间操作汇总

    Greenplum数据库时间操作与mysql有一些区别,汇总以往笔记记录下来. greenplum时间格式:'yyyy-mm-dd hh24:mi:ss.us'.'yyyy-mm-dd hh:mi:s ...

  6. 🎀SQL注入拦截工具-动态order by

    简介 业务场景经常会存在动态order by 入参情况,在处理动态 order by 参数时,需要防止SQL注入攻击.SQL注入是一种常见的安全漏洞,攻击者可以通过这种手段操纵查询来执行恶意代码. 措 ...

  7. 网络开发中的Reactor(反应堆模式)和Proacrot(异步模式)

    服务器程序重点处理IO事件,即:用户的请求读出来,反序列化,回调业务处理,回写.如果在按照面向过程的思路去写,就发挥不出CPU并发优势.那么有没有更优雅的设计方式呢? 有的兄弟,有的. Reactor ...

  8. 康谋分享 | 3DGS:革新自动驾驶仿真场景重建的关键技术

    登录后复制 随着自动驾驶技术的迅猛发展,构建高保真.动态的仿真场景成为了行业的迫切需求.传统的三维重建方法在处理复杂场景时常常面临效率和精度的挑战.在此背景下,3D高斯点阵渲染(3DGS)技术应运而生 ...

  9. fiddler抓包配置

    一.fiddler配置 打开tools-options 1.设置general,勾选对应选项 2.设置HTTPS,勾选Decrypt HTTPS traffic时,首次使用如果没有下载过fiddler ...

  10. 一文搞懂Docker Compose

    什么是Docker Compose Docker Compose 是 Docker 的一个编排管理工具,它允许你使用一个 YAML 文件来配置应用程序的服务.通过这个文件,你可以定义多个容器如何通过网 ...