1. 1 Refrain Anan Ryoko
  2. 2 镇命歌 -しずめうた- 瀧沢一留
  3. 3 Pure SCHAT10(影)
  4. 4 Lemon Soda NGC 3.14/Tenkitsune
  5. 5 summer vibe Cyan Lpegd
  6. 6 DJ Okawari - Flower Dance(钢琴原版) Oturans
  7. 7 花降らし n-buna/初音ミク
  8. 8 Lemon 米津玄師
  9. 9 明けない夜、醒めない夢 Yunomi
  10. 10 ニゲラの花束 鎖那
  11. 11 ひだまりの郷 八乙女葦菜
  12. 12 Pneumatic Tokyo EnV
  13. 13 摘星座的女孩 Rainbowets
Refrain - Anan Ryoko
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

FFT+NTT模板 笔记

由于作者水平就只能到模板,所以笔记也就写到这里。

FFT

简介

FFT是干啥的?它是用来加速多项式乘法的。我们平时经常求多项式乘法,比如(x+1)(x+3)=(x2+4x+3)(x+1)(x+3)=(x2+4x+3)。假设两个式子都是nn项(不足的补0),那朴素的算法是O(n2)O(n2)的。
那么,我们能做到O(nlogn)O(nlogn)做么?
你也许觉得不珂能。的确,不看题解,是很难自己想到正解的(除非你是欧拉费马级别的天才)

前置知识

多项式点值表示

我们平常表达多项式,都是用系数表达的。当然,还有点值表达。用平面直角坐标系上的nn个点,唯一确定一个(不超过)n1n1次的多项式。它的一个特殊形式,就是两点确定一条直线。
点值转系数表达,你只需要解一个方程组就珂以了。(高斯消元,这样是O(n3)O(n3)的)

复数

复数的定义

这个很多人知道。定义i=1i=1,即虚数单位。形如a+bia+bi的数就是复数(complex)。
复数a+bia+bi的辐角:从(0,0)(0,0)(a,b)(a,b)的线段和xx轴的夹角。这个夹角,是顺时针方向的夹角。取值范围是[0,360][0,360]

复数的几何意义

在一维数轴上,我们把一个数乘以11,相当于旋转了180180
那么,我们把一个数乘以11,相当于:乘两次是旋转180180。所以,乘一次就是旋转9090,也就是竖起来了。“竖起来”,这个概念很好表示,就是yy坐标。
那么,a+bia+bi就相当于平面直角坐标系上的点(a,b)(a,b)。当然,你也珂以把它看成一个向量,从(0,0)(0,0)(a,b)(a,b)

复数的运算

复数的加法:(a1+b1i)+(a2+b2i)=(a1+a2)+(b1+b2)i(a1+b1i)+(a2+b2i)=(a1+a2)+(b1+b2)i。(这样也就包含了减法的情况)
复数的乘法:(a1+b1i)×(a2+b2i)(a1+b1i)×(a2+b2i)
我们把括号拆开,然后把i2i2都变成11(由i的定义)。易得,它等于:(a1a2b1b2)+(a1b2+a2b1)i

(n次)单位复根

满足xn=1的所有复数解中,辐角最小且不为0的那个复数。记作wn。不难发现,所有满足条件的x,用向量表示后,把单位圆n等分。
举个栗子,n=6的时候,解的分布是这样的:
blog1.jpg
其中,ωn就是图中标出来的,辐角大于0且最小的那个向量。

单位复根的性质
  1. w2k2n=wkn(珂以把wkn看成是360deg×kn,这条证毕)
  2. wk+n/2n=wkn。(这里n/2不取整,就是小数) (把n/2看成是转半圈,转半圈也就是x,y坐标都变负,这条也证毕)

正式开始!

上面不是说了点值表示么。对于两个用点值表示的多项式,只要把对应的点值乘起来即珂。
但是,我们要取n个点(DFT),每次O(n)求值,不是要O(n2)了么?
而且,把点值转换成系数(插值,IDFT)的过程,不是要O(n3)么?
因此,我们的主干思想是:利用单位复根的性质,巧妙的求值/插值,使得我们在O(nlogn)的时间内完成这些操作。
简图(远航之曲大佬的图):

DFT

就是快速带入点值的过程。
我们的多项式:A(x)=a0x0+a1x1+a2x2+an1xn1。其中a0,a1an是系数(题目给定)。
接下来,我们设n=2k。不足的地方用0补齐。
把它的系数按下表奇偶分组(指数还是顺序下来的):
A0(x)=a0x0+a2x1+a4x2+an2xn/21
A1(x)=a1x0+a3x1+a5x2+an1xn/21
易得A(x)=A0(x2)+xA1(x2)
那么,我们代入x=w0n,wn,w2nwn1n
考虑求前面n/2个,然后直接得到后面n/2个。令k[0,n/2),则:
A(wkn)=A0(w2kn)+wknA1(w2kn)
然后我们再代入wk+n/2n=wkn
A(wk+n/2n)=A0(w2nk)A1(w2nk)
我们发现,wk+n/2n=wkn。然而A0,A1里面是x2,所以取负不影响A0A1的结果,只有A1前面那一项有一个正负号的区别!
所以,我们求出前一半,就珂以O(n)求出后一半。
这样显然是O(nlogn)的。

DFT的实现优化

刚刚做完O(nlogn)的式子。但是,实现的时候,递归似乎太慢了,还不如暴力来的快。
我们观察一下,被奇偶分组后的下标。
blog2.jpg
(草 图)
转换一下二进制:
000 001 010 011 100 101 110 111
变成:
000 100 010 110 001 101 011 111
相当于每个二进制数位反过来了。
然后我们通过递推,推出最后的状态。然后不断合并,合并成答案。成功的把递归转化掉了。这样还是O(nlogn),但是快了很多!

IDFT

IDFT,就是我们已知一个点值表示的多项式,而且代入的点值还是w0n,w1nwn1n

我们设出系数a0,a1..an1,列出方程:

{a0(w0n)0+a1(w0n)1...+an1(w0n)n1=A(w0n)a0(w1n)0+a1(w1n)1...+an1(w1n)n1=A(w1n)...a0(wn1n)0+a1(wn1n)1...+an1(wn1n)n1=A(wn1n)

用矩阵表达:

[(w0n)0(w0n)1...(w0n)n1(w1n)0(w1n)1...(w1n)n1...(wn1n)0(wn1n)1...(wn1n)n1]×[a0a1...an1]=[A(w0n)A(w1n)...A(wn1n)]

设这个式子是“珂朵莉IDFT①式”
然后设矩阵DD中的每一项和左边那个矩阵

V=[(w0n)0(w0n)1...(w0n)n1(w1n)0(w1n)1...(w1n)n1...(wn1n)0(wn1n)1...(wn1n)n1]

中对应位置上的项,都是倒数。
已证,D×V=n×In,其中Inn阶单位矩阵。
那也就是,D=1nV1
把“珂朵莉IDFT①式”中,左右两边同时乘一个D=1nV1
易得:

[a0a1...an1]=1nD[A(w0n)A(w1n)...A(wn1n)]

那么我们把矩阵V中,所有项取倒数,然后做一遍DFT即珂。最后记得除一个n

模板题的代码

洛谷 3803

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
#define N 3000006 //空间的理论下限
//2097153=2^21+1
#define real double
#define Pi (3.14159265358979323846264338)
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.Start(u),__v=G.To(i);~i;i=G.Next(i),__v=G.To(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define iter(a,p) (a.begin()+p)
void R1(int &x)
{
x=0;char c=getchar();int f=1;
while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=(f==1)?x:-x;
}
void Rd(int cnt,...)
{
va_list args;
va_start(args,cnt);
F(i,1,cnt)
{
int* x=va_arg(args,int*);R1(*x);
}
va_end(args);
}
struct cp{real R,I;}; //复数类
//R: 实部,a+bi中的a
//I:虚部,a+bi中的b
cp operator+(cp a,cp b){return (cp){a.R+b.R,a.I+b.I};}
cp operator-(cp a,cp b){return (cp){a.R-b.R,a.I-b.I};}
cp operator*(cp a,cp b){return (cp){a.R*b.R-a.I*b.I,a.R*b.I+a.I*b.R};}

int n,m;
cp a[N],b[N];
void Input()
{
Rd(2,&n,&m);
F(i,0,n) {int x;R1(x);a[i].R=x;}
F(i,0,m) {int x;R1(x);b[i].R=x;}
}

int r[N],lim;
void FFT(cp A[],int type)
//type=1: DFT
//type=-1: IDFT
{
F(i,0,lim) if (i<r[i]) swap(A[i],A[r[i]]);
for(int mid=1;mid<lim;mid<<=1) //合并区间的长度
//每次合并两个长度为mid的区间
{
cp Wn=(cp){cos(Pi/mid),type*sin(Pi/mid)};
//把虚部乘以-1,相当于变成一个实数,其值为向量长度
//由于向量在单位圆上,长度为1,就相当于取倒数了
for(int j=0;j<lim;j+=(mid<<1))
{
cp w=(cp){1,0}; //Wn^0
for(int k=0;k<mid;++k,w=w*Wn) //w:不断代入Wn^k
{
cp X=A[j+k],Y=w*A[j+mid+k];
//DFT的合并式子
A[j+k]=X+Y;
A[j+mid+k]=X-Y;
}
}
}
}
void Soviet()
{
int l=0;lim=1;
while(lim<=n+m) lim<<=1,++l;
F(i,0,lim) r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
FFT(a,1);FFT(b,1); //两个DFT
F(i,0,lim) a[i]=a[i]*b[i]; //点值乘法
FFT(a,-1); //IDFT
F(i,0,n+m) printf("%d ",(int)(a[i].R/lim+0.5)); //还要乘一个1/lim
//+0.5是取四舍五入
putchar('\n');
}

#define Flan void
Flan IsMyWife()
{
Input();
Soviet();
}
}
int main(){
Flandre_Scarlet::IsMyWife();
getchar();getchar();
return 0;
}

NTT

NTT本质上就是把FFT中的单位复根换成一个有相同性质的整数:原根。
只要记住998244353的原根是3即珂。
然后和FFT同样的方法去写就珂以了,也是DFT+IDFT。
就是把里面单位复根换成原根,一样写即可。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

#include <bits/stdc++.h>using namespace std;
namespace Flandre_Scarlet
{
#define N 2666666
#define mod 998244353
#define Gi 332748118 //3^(-1) mod 998244353
#define int long long
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.Start(u),__v=G.To(i);~i;i=G.Next(i),__v=G.To(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define iter(a,p) (a.begin()+p)
void R1(int &x)
{
x=0;char c=getchar();int f=1;
while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=(f==1)?x:-x;
}
void Rd(int cnt,...)
{
va_list args;
va_start(args,cnt);
F(i,1,cnt)
{
int* x=va_arg(args,int*);R1(*x);
}
va_end(args);
}

int n,m;
int a[N],b[N];
void Input()
{
Rd(2,&n,&m);
F(i,0,n) R1(a[i]);
F(i,0,m) R1(b[i]);
}
int qpow_p(int a,int b,int m) //正数的快速幂
{
int r=1;
while(b)
{
if (b&1) r=r*a%m;
a=a*a%m,b>>=1;
}
return r;
}
int qpow(int a,int b,int m) //支持负指数的快速幂(就是先求快速幂,然后求一个逆元)
{
if (b==0) return 1;
else if (b<0) return qpow_p(qpow_p(a,-b,m),m-2,m);
else return qpow_p(a,b,m);
}
int r[N],lim;
void NTT(int A[N],int type)
{
F(i,0,lim) if (i<r[i]) swap(A[i],A[r[i]]);
for(int mid=1;mid<lim;mid<<=1)
{
int Wn=qpow(qpow(3,type,mod),(mod-1)/(mid<<1),mod);
for(int j=0;j<lim;j+=(mid<<1))
{
int w=1;
for(int k=0;k<mid;k++,w=(w*Wn)%mod)
{
int X=A[j+k],Y=w*A[j+mid+k]%mod;
A[j+k]=(X+Y)%mod;
A[j+mid+k]=(X-Y+mod)%mod;
}
}
}
}
void Soviet()
{
lim=1ll;int l=0;
while(lim<=n+m) lim<<=1ll,++l;
F(i,0,lim) r[i]=(r[i>>1ll]>>1ll)|((i&1ll)<<(l-1));
NTT(a,1);NTT(b,1);
F(i,0,lim) a[i]=(a[i]*b[i])%mod;
NTT(a,-1);
int iv=qpow(lim,-1,mod);
F(i,0,n+m) printf("%lld ",a[i]*iv%mod); //*iv相当于除以一个lim
putchar('\n');
}

#define Flan void
Flan IsMyWife()
{
Input();
Soviet();
}
#undef int //long long
}
int main(){
Flandre_Scarlet::IsMyWife();
getchar();getchar();
return 0;
}

NTT的好处和坏处

好处: 和FFT相比,把浮点数换成整数。常数很小,而且避免了精度问题
坏处: 小心溢出!(超过NTT模数也是一种溢出,会导致答案不同)(如果让你对某个数取模那另说)

w