多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/常用套路【入门】

原文链接https://www.cnblogs.com/zhouzhendong/p/Fast-Fourier-Transform.html

多项式 之 快速傅里叶变换(FFT)/数论变换(NTT)/例题与常用套路【入门】

 

前置技能

  • 对复数以及复平面有一定的了解
  • 对数论要求了解:逆元,原根,中国剩余定理
  • 对分治有充足的认识
  • 对多项式有一定的认识,并会写 $O(n^2)$ 的高精度乘法

 

本文概要

  • 多项式定义及基本卷积形式
  •  $Karatsuba$ 乘法
  • 多项式的系数表示与点值表示,以及拉格朗日插值法
  • 复数与单位根
  • 快速傅里叶变换 $(FFT)$ 
  • 数论变换 $(NTT)$ 
  • 分治 $FFT$ 
  • 拆系数 $FFT$ 和三模数 $NTT$ 
  • 例题与套路

前言

   近年来,多项式理论进入中国 $OI$ 圈,在中国 $OI$ 圈逐渐占据一方,是一个值得我们去研究的理论。现在, $OI$ 题中出现次数越来越频繁的多项式题,也鼓励了许多 $OIer$ 去学习多项式。

  作为多项式的一个算法—— $FFT$ ,他是一个至关重要的存在。比如,对于初学高精度时的你,是否听说过高精度乘法可以 $O(n\log n)$ ? $FFT$ 可以来解决一类多项式卷积,是生成函数一系列操作的基础,可以解决很多计数问题。

  于是,菜鸡博主去学了一下 $FFT$ ,写了这篇总结。

 

多项式定义及基本卷积形式

多项式

  定义 多项式 为形如下式的代数表达式。

$$P(x)=\sum_{i=0}^{n}a_ix^i=a_0+a_1x+a_2x^2+\cdots+a_nx^n$$

  其中 $a_0,a_1,a_2,\cdots,a_n$ 称为多项式的 系数

   $x$ 没有确定的值。

  最高次项的指数 $n$ 叫做多项式的 度 $(Degree,n=deg\ P)$ ,也可以说是多项式的 次数

多项式基本卷积形式

  下面的这个多项式卷积就是多项式乘法。

  定义两个多项式 $g(x),f(x)$ ,设他们的度数分别为 $n,m$ ,则卷积具有如下形式:(设 $g_i$ 为 $g$ 的 $i$ 次项系数, $f_i$ 为 $f$ 的 $i$ 次项系数)

$$h(x)=g(x)f(x)=\sum_{i=0}^{n}\sum_{j=0}^{m}g_if_jx^{i+j}$$

$$=\sum_{i=0}^{n+m}\sum_{j=0}^{i}g_jf_{i-j}x^i$$

  请务必理解并记住第二行的卷积式,这将会在后面不停的出现。

  我们显然可以通过公式来 $O(nm)$ 得到卷积结果。

 

 $Karatsuba$ 乘法

  针对这种卷积, $Karatsuba$ 提出了下面的这种方法:

  对于多项式 $F$ ,我们令 $n=deg\ F+1$ 。

  即多项式可以写成这个形式: $F(x)=\sum_{i=0}^{n-1}a_ix^i$ 。

  令 $F(x)=F_0(x)+x^{\frac n2}F_1(x),G(x)=G_0(x)+x^{\frac n2}G_1(x)$ 。

  其中, $deg\ F_0=deg\ F_1=deg\ G_0=deg\ G_1=\frac n2$ 。

  则

$$(F\times G)(x)=(F_0\times G_0)(x)+x^{\frac n2}(F_0\times G_1+F_1\times G_0)(x)+x^n(F_1\times G_1)(x)$$

  令 $M(x)=((F_0+F_1)\times(G_0+G_1))(x)$ 

  我们会开心的发现:

$$(F_0\times G_1+F_1\times G_0)(x)=M(x)-(F_0\times G_0)(x)-(F_1\times G_1)(x)$$

  于是我们只需要计算三个多项式卷积 $M(x),(F_0\times G_0)(x)-(F_1\times G_1)(x)$ 即可。

  我们采用分治的做法,于是时间复杂度为:

$$T(n)=3T(\frac n2)+O(n)=n^{\log_23}\approx n^{1.585}$$

多项式的系数表示与点值表示,以及拉格朗日插值法

系数表示

  (令 $n=deg\ F$ )

  这里拿出了最开始提到的多项式:

$$f(x)=a_0+a_1x+a_2x^2+\cdots+a_nx^n$$

  把 $a$ 数组看作 $n+1$ 维向量 $\vec{a}=(a_0,a_1,\cdots,a_n)$ ,其 系数表示 就是向量 $\vec{a}$ 。

点值表示

  (令 $n=deg\ F$ )

  取任意 $n+1$ 个不同的 $x_0,x_1,\cdots,x_n$ 代入多项式进行求值,得到了 $n+1$ 个不同的二元组 $(x_0,F(x_0)),(x_1,F(x_1)),\cdots,(x_n,F(x_n))$ 。

  我们称这 $n+1$ 个点表示多项式的方法叫做多项式的 点值表示 

  这里要注意,多项式的点值表示有无数种,但是多项式的系数表示是唯一的。

拉格朗日插值法

  实现系数表示到点值表示的变换,我们可以直接把 $x$ 代入求解,复杂度 $O(n^2)$ 。

  但是点值表示转系数表示就没这么简单了。

  这里首先提一点:虽然同一个多项式的点值表示有无数种,但是这些点值表示都能唯一的确定一个多项式,唯一的确定一个系数表示。

  从点值表示转到系数表示,我们可以使用插值法。

  其中拉格朗日插值法能够在 $O(n^2)$ 的复杂度内得到系数表示。

  具体在这里不展开介绍了,可以参见:

  https://www.zhihu.com/question/58333118/answer/262507694

复数与单位根

复数的定义

  复数 $(Complex)$ 由实部和虚部组成。

  可以写成如下形式:

$$A=a+ib,(a,b\in \mathbb R,A\in\mathbb C)$$

  其中 $A$ 就是一个复数。

  其中 $i=\sqrt{-1}$ ,为虚数单位。

  在后面的公式中要注意区分虚数单位 $i$ 和求和指标 ($\sum$) $i$ 。

  显然这里可以列举三个 $FFT$ 常用的复数运算:

  $(A_1,A_2\in\mathbb C,a_1,b_1,a_2,b_2\in\mathbb R)$

$$A_1+A_2=(a_1+ib_1)+(a_2+ib_2)=(a_1+a_2)+i(b_1+b_2)$$

$$A_1-A_2=(a_1+ib_1)-(a_2+ib_2)=(a_1-a_2)+i(b_1-b_2)$$

$$A_1\times A_2=(a_1+ib_1)(a_2+ib_2)=a_1a_2+i^2b_1b_2+ia_1b_2+ia_2b_1\\=(a_1a_2-b_1b_2)+i(a_1b_2+a_2b_1)$$

复平面

  复平面是个二维平面,其中横轴代表实数,称为实轴。纵轴代表虚数,称为虚轴。

  定义复平面上从原点出发的向量$\vec{a}=(a,b)$表示虚数$a+ib$。

  例如:

 

  其中的 $5$ 条蓝色向量就代表了 $5$ 个虚数。

  关于复数乘法,有一个口诀:模长相乘,幅角相加。

  模长就是复数在复平面上表示的向量的模长,幅角就是以正实轴为始边,这个向量为终边所构成的角。

单位根

   $n$ 次单位根 $\omega_n\in\mathbb C$ ,满足 $\omega_n^n=1$ 。

  显然 $n$ 次方程有 $n$ 个解,把他们写在复平面上面,可以把单位圆等分成 $n$ 份。由于复数乘法模长相乘,所以模长永远是 $1$ ,说明他们总在单位圆上。由于幅角相加,得到他们等分单位圆。

  于是我们可以写出单位根的表达式:

  记 $\omega_n=\cos(\frac{2\pi}{n})+i\sin(\frac{2\pi}{n})$ ,则 $n$ 次单位根就是 $\omega_n^0, \omega_n^1, \cdots, \omega_n^{n-1}$ 。

  通项公式, $\omega_n^i=\cos(\frac{2i\pi}{n})+i\sin(\frac{2i\pi}{n})\ \ \ \ (0\leq i<n)$ 

  当然,在数学上,单位根还有这种定义: $\omega_n=e^{\frac{2\pi i}{n}}$ ,读者可以尝试通过这个来推导前几个式子,这里不展开介绍。

  单位根有一些优秀的性质。

 

  定理1:相消定理

  对于任意整数 $n\geq 0,k\geq 0,d\geq 0$ ,有:

$$\omega_{dn}^{dk}=e^{\frac{2\pi dki}{dn}}=e^{\frac{2\pi ki}{n}}=\omega_n^k$$

  即:

$$\omega_{dn}^{dk}=\omega_{n}^{k}$$

 

  定理2:折半定理

  对于任意的偶数 $n\geq 0,k\geq 0$ ,有

$$\omega_{n}^{k}=\omega_{\frac n2}^{\frac k2}$$

  这个由定理1显然可得。

 

  此外,我们还可以从复平面图像的角度理解单位根。

  观察任何一个单位根 $\omega_{n}^{i}$ ,

$$\omega_{n}^{i+1}=\omega_{n}^{i}\times\omega_{n}^{1}$$

  观察其图像,会发现 $\times\omega_{n}^{1}$ 的效果就是把原复数在复平面上的向量绕原点逆时针旋转$\frac{2\pi}{n}$的角度。

  类似的, $\times\omega_{n}^{1}$ 的效果相反,为顺时针方向。

  再有,显然, $\omega_{n}^{-i}=\omega_{n}^{n-i}$ 。

  于是对于整数 $i$ ,如果从 $\omega_{n}^{0}$ 逆时针旋转一定角度得到的向量为单位根 $\omega_{n}^{i}$ ,那么顺时针旋转相同的角度也必然会得到 $\omega_{n}^{-i}=\omega_{n}^{n-i}$ 。

  于是如果把所有 $n$ 次单位根在复平面上画出来,他们是上下对称的。

  性质3

  所以如果令 $\omega_{n}^{k}=a+ib$ ,则 $\omega_{n}^{-k}=a-ib\ \ \ \ (a,b\in \mathbb R,\sqrt{a^2+b^2}=1)$ ,即 $\omega_n^{-k}=conj(\omega_n^k)$ 。

 

  考虑到 $\omega_{n}^{n}$ 是绕着原点转了一圈,那么 $\omega_{n}^{\frac n2}$ 就是转了半圈,所以 $\omega_{n}^{\frac n2}=-1$ 。

  所以 $\omega_{n}^{i}$ 与 $\omega_{n}^{i+\frac n2}$ 在单位圆上的位置是相对的,因为转了半圈。换句话说:

  性质4

$$\omega_{n}^{i+\frac n2}=\omega_{n}^{\frac n2}\cdot\omega_{n}^{i}=-\omega_{n}^{i}$$

快速傅里叶变换(Fast Fourier Transform, FFT)

  回忆一下之前的多项式卷积:

$$h(x)=\sum_{i=0}^{n+m}\sum_{j=0}^{i}g_jf_{j-i}x^i$$

  我们要快速求 $h(x)$ 的每一项系数。

  系数表示并不能支持快速的卷积。

  但是在点值表示下,却可以在 $O(n)$ 复杂度内快速卷积。

  目前的瓶颈在于系数表示与点值表示的快速的相互转化。

  由于点值表示有很多种,只要你选择的 $x$ 互不相同即可。于是我们可以考虑选择一些特殊的 $x$ 。

  注意,后面为了方便,设多项式的度为 $n-1,(n=2^a,a\in\mathbb Z)$ ,如果次数不足则高位补上系数 $0$ ,保证任何运算过程中多项式的真的度都小于 $n$ 。

离散傅里叶变换

  设有多项式

$$F(x)=\sum_{i=0}^{n-1}a_ix^i$$

  将 $n$ 个不同的 $n$ 次单位根 $\omega_{n}^{0},\omega_{n}^{1},\cdots,\omega_{n}^{n-1}$ 代入到 $F(x)$ 中,得到点值表达式:

$$\vec{y}=(F(\omega_{n}^{0}),F(\omega_{n}^{1}),\cdots,F(\omega_{n}^{n-1}))$$

  于是点值向量 $\vec{y}$ 就叫做系数向量 $\vec{a}=(a_0,a_1,\cdots,a_{n-1})$ 的离散傅里叶变换 $(Discrete\ Fourier\ Transform,\ DFT)$ 。

  但是直接代入求值是 $O(n^2)$ 的,我们需要一个更快的求值方法。

  令 $F_0(x)=\sum_{i=0}^{\frac n2-1}a_{2i}x^i,F_1(x)=\sum_{i=0}^{\frac n2-1}a_{2i+1}x^i$

  则:(下面的推导会用到之前介绍过的单位根性质的第2条和第4条)

$$F(x)=F_0(x^2)+xF_1(x^2)$$

$$F(\omega_{n}^{i})=F_0(\omega_{n}^{2i})+\omega_{n}^{i}F_1(\omega_{n}^{2i})=F_0(\omega_{\frac n2}^{i})+\omega_{n}^{i}F_1(\omega_{\frac n2}^{i})$$

$$F(\omega_{n}^{i+\frac n2})=F(-\omega_{n}^{i})=F_0(\omega_{n}^{2i})-\omega_{n}^{i}F_1(\omega_{n}^{2i})=F_0(\omega_{\frac n2}^{i})-\omega_{n}^{i}F_1(\omega_{\frac n2}^{i})$$

  注意: $deg\ F_0=deg\ F_1=\frac n2$ 。

  于是我们可以继续分治,对于 $F_0$ 和 $F_1$ 再继续同样的操作。

  时间复杂度:

$$T(n)=2T(\frac n2)+O(n)=O(n\log n)$$

逆离散傅里叶变换

  现在你需要快速的把点值表达式转换成系数表达式。

  与刚才的离散傅里叶变换相反,系数向量 $\vec{a}=(a_0,a_1,\cdots,a_{n-1})$ 就叫做点值向量 $\vec{y}$ 的逆离散傅里叶变换 $(Inverse\ Discrete\ Fourier\ Transform,\ IDFT)$ 

  首先,我们把刚才的 $DFT$ 过程用矩阵来表示:

$$\begin{equation}\begin{bmatrix} (\omega_n^0)^0 & (\omega_n^0)^1 & \cdots & (\omega_n^0)^{n-1} \\ (\omega_n^1)^0 & (\omega_n^1)^1 & \cdots & (\omega_n^1)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{n-1})^0 & (\omega_n^{n-1})^1 & \cdots & (\omega_n^{n-1})^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} A(\omega_n^0) \\ A(\omega_n^1) \\ \vdots \\ A(\omega_n^{n-1}) \end{bmatrix}\end{equation}$$

  我们记左侧的系数矩阵为 $\mathbf V$ ,构造矩阵 $d_{i,j}=(\omega_{n}^{-i})^j$

$$\mathbf D = \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix}$$

  设 $\mathbf E=\mathbf D\cdot\mathbf V$ ,则:

$$\begin{eqnarray*} e_{ij} &=& \sum_{k=0}^{n-1} d_{ik} v_{kj} \\ &=& \sum_{k=0}^{n-1} \omega_n^{-ik}\omega_n^{kj} \\ &=& \sum_{k=0}^{n-1} \omega_n^{k(j-i)}\\&=&\large{\begin{cases}n&\text{$(i=j)$}\\ \sum_{k=0}^{n-1}(\omega_{n}^{j-i})^k=\frac{1-(\omega_{n}^{j-i})^n}{1-\omega_{n}^{j-i}}=0&\text{$(i\neq j)$}\end{cases}}\end{eqnarray*}$$

  于是我们发现了一个非常特殊的性质:

   $\mathbf E$ 是单位矩阵的 $n$ 倍。

  也就是 $\frac 1n\mathbf D=\mathbf V^{-1}$ 。

  于是我们将 $\frac 1n\mathbf D$ 在 $(1)$ 式两侧左乘,得到:

$$\begin{equation*} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \frac{1}{n} \begin{bmatrix} (\omega_n^{-0})^0 & (\omega_n^{-0})^1 & \cdots & (\omega_n^{-0})^{n-1} \\ (\omega_n^{-1})^0 & (\omega_n^{-1})^1 & \cdots & (\omega_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega_n^{-(n-1)})^0 & (\omega_n^{-(n-1)})^1 & \cdots & (\omega_n^{-(n-1)})^{n-1} \end{bmatrix} \begin{bmatrix} A(\omega_n^0) \\ A(\omega_n^1) \\ \vdots \\ A(\omega_n^{n-1}) \end{bmatrix} \end{equation*}$$

  于是只需要把 $\omega_{n}^{i}$ 换成 $\omega_{n}^{-i}$ (根据单位根性质4,只需要把 $\omega_{n}^{i}$ 的虚部取其相反数即可),然后 $DFT$ ,然后将所得的结果除以 $n$ ,就可以实现 $IDFT$ 了。

递归版FFT代码

  有同学认为需要加一份递归版的代码。可惜是博主没有写过递归版的。于是就从网上拉了一份QAQ。

void fft(int n, complex<double>* buffer, int offset, int step, complex<double>* epsilon)
{
	if(n == 1) return;
	int m = n >> 1;
	fft(m, buffer, offset, step << 1, epsilon);
	fft(m, buffer, offset + step, step << 1, epsilon);
	for(int k = 0; k != m; ++k)
	{
		int pos = 2 * step * k;
		temp[k] = buffer[pos + offset] + epsilon[k * step] * buffer[pos + offset + step];
		temp[k + m] = buffer[pos + offset] - epsilon[k * step] * buffer[pos + offset + step];
	}

	for(int i = 0; i != n; ++i)
		buffer[i * step + offset] = temp[i];
}
//http://blog.miskcoo.com/2015/04/polynomial-multiplication-and-fast-fourier-transform#IDFT

迭代版FFT与蝴蝶操作

  你显然已经可以递归实现 $FFT$ 了。但是递归实现 $FFT$ 常数较大,代码又长,一般不写。

  假设现在有 $8$ 个数要进行 $DFT$ ,我们来看看递归的时候,每一个数的顺序以及二进制位的情况。

$$\begin{vmatrix}000&&001&&010&&011&&100&&101&&110&&111\\0&&1&&2&&3&&4&&5&&6&&7\\0&&2&&4&&6&|&1&&3&&5&&7\\0&&4&|&2&&6&|&1&&5&|&3&&7\\0&|&4&|&2&|&6&|&1&|&5&|&3&|&7\\000&&100&&010&&110&&001&&101&&011&&111\end{vmatrix}$$

  稍微观察一下,你就会发现,分治到边界之后的下标是原下标的二进制位翻转。

  于是我们只需要预处理每一个数的二进制位翻转后的结果,并在 $FFT$ 开始前交换所有数与他翻转之后的数。具体可以参见后面的代码。

蝴蝶操作

  在合并两个子问题的过程中,假设 $A_0(\omega_{\frac n2}^{k})$ 和 $A_1(\omega_{\frac n2}^{k})$ 分别保存在 $a_k$ 和 $a_{\frac n2+k}$ 中,而 $A(\omega_{n}^{k})$ 和 $A(\omega_{n}^{k+\frac n2})$ 将会被放在之后的 $a_k$ 和 $a_{\frac n2+k}$ 中,为了避免覆盖原值出错,我们加入一个临时变量,并实现以下三个操作:

$$tmp\leftarrow \omega_{n}^{k}\times a_{\frac n2+k}$$

$$a_{\frac n2+k}\leftarrow a_k-tmp$$

$$a_k\leftarrow a_k + tmp$$

  这个操作被叫做蝴蝶操作。

迭代版FFT模版 -UOJ#34多项式乘法

#include <bits/stdc++.h>
using namespace std;
const int N=1<<20;
const double PI=acos(-1.0);
struct C{
	double r,i;
	C(){}
	C(double a,double b){r=a,i=b;}
	C operator + (C x){return C(r+x.r,i+x.i);}
	C operator - (C x){return C(r-x.r,i-x.i);}
	C operator * (C x){return C(r*x.r-i*x.i,r*x.i+i*x.r);}
}a[N],b[N],w[N];
int A,B,n,L,R[N];
void FFT(C a[],int n){
	for (int i=0;i<n;i++)
		if (R[i]>i)
			swap(a[R[i]],a[i]);
	for (int t=n>>1,d=1;d<n;d<<=1,t>>=1)
		for (int i=0;i<n;i+=(d<<1))
			for (int j=0;j<d;j++){
				C tmp=w[t*j]*a[i+j+d];
				a[i+j+d]=a[i+j]-tmp;
				a[i+j]=a[i+j]+tmp;
			}
}
int main(){
	scanf("%d",&A);A++;
	scanf("%d",&B);B++;
	for (int i=0;i<A;i++)
		scanf("%lf",&a[i].r);
	for (int i=0;i<B;i++)
		scanf("%lf",&b[i].r);
	for (n=1,L=0;n<=A+B;n<<=1,L++);
	for (int i=0;i<n;i++){
		R[i]=(R[i>>1]>>1)|((i&1)<<(L-1));
		w[i]=C(cos(2.0*i*PI/n),sin(2.0*i*PI/n));
	}
	FFT(a,n),FFT(b,n);
	for (int i=0;i<n;i++)
		a[i]=a[i]*b[i],w[i].i=-w[i].i;
	FFT(a,n);
	A--,B--;
	for (int i=0;i<=A+B;i++)
		printf("%d ",int(a[i].r/n+0.5));
	
	return 0;
}

数论变换(Number-Theoretic Transform,NTT)

   $FFT$ 虽然能快速处理卷积,但是它也有很大的弊端。精度问题有时会导致一些错误。而且,有许多题目涉及了取模,比如 $998244353$ ,复数域下的 $DFT$ 精度更是暴露无遗。于是考虑是否有模意义下的这种算法。于是,便出现了快速数论变换($Fast\ Number-Theoretic\ Transform,\ FNT$)。

  虽然之前列举了一些单位根的性质,但是 $FFT$ 用到的却和我列举的有些差别。

  于是我们回顾一下 $FFT$ 利用了单位根的什么性质。

  1.  $\omega_{n}^{n}=1$

  2.  $\omega_{n}^{0},\omega_{n}^1,\cdots,\omega_{n}^{n-1}$ 互不相同,满足了点值表示的条件。

  3.  $\omega_n^2=\omega_{\frac{n}{2}}, \omega_n^{\frac{n}{2}+k}=-\omega_n^k$ ,这个是分治的基础。

  4.  $\omega_{n}^{-k}=conj(\omega_n^k)$

    $\sum_{k=0}^{n-1}(\omega_n^{j-i})^k=\begin{cases}0&\text{$(i\neq j)$}\\n&\text{$(i=j)$}\end{cases}$

    这个是$IDFT$的基础。

原根

  我们需要找满足上面的性质的整数。

  于是我们找到了原根。

  对于一个质数 $p$ ,其 原根 $g$ 满足 $g^0,g^1,g^2,\cdots,g^{p-2}$ 在模 $p$ 意义下两两不同。

  可以发现, $n$ 需要是 $p-1$ 的因数,才能满足条件。由于 $n$ 是 $2$ 的幂,所以对 $p$ 也有一定的要求。

   $p$ 得是形如 $r\cdot 2^k+1$ 的素数,其中 $2^k\geq n$ 。

  有一些非常常见的 $NTT$ 模数:

    $998244353=119\times 2^{23}+1$ ,原根为 $3$ 。

    $1004535809=479\times 2^{21}+1$ ,原根为 $3$ 。

    更多 $NTT$ 模数 $\Longrightarrow$ http://blog.miskcoo.com/2014/07/fft-prime-table

  现在我们来证明一下原根满足上面的那些性质。

  令 $g_n^n\equiv 1\ \pmod p$ 且 $g_n ^ 0,g_n ^ 1,\cdots g_n ^ {n-1}$ 互不相同,则 $g_n \equiv g^r \pmod p$(这里的 $r=(p-1)/n$ 与之前提到的无关),等价于 $\omega_n$ 。

  1.  $\omega_{n}^{n}=1$

  根据定义,显然成立。

  2.  $\omega_{n}^{0},\omega_{n}^1,\cdots,\omega_{n}^{n-1}$互不相同

  根据原根的定义,也显然成立。

  3.  $\omega_n^2=\omega_{\frac{n}{2}}, \omega_n^{\frac{n}{2}+k}=-\omega_n^k$

  由于 $g_n\equiv g^r\ \pmod p$ ,其中由于 $nr=p-1$ ,当 $n$ 取 $\frac n2$ 时, $r$ 取 $2r$ ,所以 $g_{\frac n2}\equiv g^{2r}\equiv(g^r)^2\equiv g_n^2\ \pmod p$ 。

  由费马小定理可得 $g^{p-1}-1\equiv (g^{\frac{p-1}2}+1)(g^{\frac{p-1}2}-1)\equiv 0\pmod p$ ,所以 $g^{\frac{p-1}2}\equiv \pm 1\pmod p$ 。又由于 $g$ 为原根,满足第 $2$ 条性质。由于 $g^0\equiv 1\pmod p$ ,所以 $g^{\frac{p-1}2}\equiv -1\pmod p$ 。于是:

$$g_{n}^{\frac n2}\equiv g^{\frac{p-1}2}\equiv -1\pmod p$$

  即 $g_n^{\frac n2+k}\equiv g_n^k\times g_n^{\frac n2}\equiv -g_n^k\pmod p$ 。

  4.  $\sum_{k=0}^{n-1}(\omega_n^{j-i})^k=\begin{cases}0&\text{$(i\neq j)$}\\n&\text{$(i=j)$}\end{cases}$

  由于 $4$ 的第一项不是很重要,所以可以不管(直接逆元算算就好了)。

$$\sum_{k=0}^{n-1}g_n^{k(j-i)}\equiv\begin{cases}n&\text{$(i=j)$}\\ \sum_{k=0}^{n-1}(g_{n}^{j-i})^k\equiv\frac{1-(g_{n}^{j-i})^n}{1-g_{n}^{j-i}}\equiv 0 &\text{$(i\neq j)$}\end{cases}\pmod p$$

  于是,综上所述,原根满足了 $FFT$ 要用到的单位根的性质,于是我们可以把单位复根替换成原根,再写个和 $FFT$ 差不多的就可以了。

NTT参考代码 - 51Nod1028 大数乘法 V2

 

#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cmath>
#include <cstring>
using namespace std;
typedef long long LL;
const LL mod=998244353;
const int N=1<<20;
LL Pow(LL x,LL y){
	if (!y)
		return 1LL;
	LL xx=Pow(x,y/2);
	xx=xx*xx%mod;
	if (y&1LL)
		xx=xx*x%mod;
	return xx;
}
LL A,B,a[N],b[N],R[N],g[N],n,L;
char str[N];
void read(){
	scanf("%s",str);
	A=strlen(str);
	for (int i=0;i<A;i++)
		a[A-i-1]=str[i]-'0';
	scanf("%s",str);
	B=strlen(str);
	for (int i=0;i<B;i++)
		b[B-i-1]=str[i]-'0';
}
void NTT(LL a[N],int n){
	for (int i=0;i<n;i++)
		if (i<R[i])
			swap(a[i],a[R[i]]);
	for (int d=1,t=n>>1;d<n;d<<=1,t>>=1)
		for (int i=0;i<n;i+=(d<<1))
			for (int j=0;j<d;j++){
				LL tmp=g[t*j]*a[i+j+d]%mod;
				a[i+j+d]=(a[i+j]-tmp+mod)%mod;
				a[i+j]=(a[i+j]+tmp)%mod;
			}
}
int main(){
	read();
	for (n=1,L=0;n<=A+B;n<<=1,L++);
	for (int i=0;i<n;i++)
		R[i]=(R[i>>1]>>1)|((i&1)<<(L-1));
	g[0]=1,g[1]=Pow(3,(mod-1)/n);
	for (int i=2;i<n;i++)
		g[i]=g[i-1]*g[1]%mod;
	NTT(a,n),NTT(b,n);
	for (int i=0;i<n;i++)
		a[i]=a[i]*b[i]%mod;
	g[0]=1,g[1]=Pow(g[1],mod-2);
	for (int i=2;i<n;i++)
		g[i]=g[i-1]*g[1]%mod;
	NTT(a,n);
	LL Inv=Pow(n,mod-2);
	for (int i=0;i<n;i++)
		a[i]=a[i]*Inv%mod;
	for (int i=0;i<n-1;i++)
		a[i+1]+=a[i]/10,a[i]%=10;
	int d;
	for (d=n-1;d&&!a[d];d--);
	for (int i=d;i>=0;i--)
		putchar(a[i]+'0');
	return 0;
}

 

注意,从本节以后,请注意观察式子中是否有卷积形式

"$\Huge{a_i=\sum_{j=0}^{i}b_jc_{i-j}}$"。

 

分治FFT  

  顾名思义,分治 $FFT$ 就是分治再加上 $FFT$ 嘛。

  考虑有如下的递推式:

$$a_i=\sum_{j=1}^{i}k_ja_{i-j}$$

  其中 $k$ 数组以及 $a_0$ 给出。

  我们发现这个式子非常像多项式卷积的形式,但是显然不能所有的 $a_i$ 一起计算,就是说一次 $FFT$ 显然不能解决问题。

  于是我们可以分治 $FFT$ 。

  定义 $solve(L,R)$ 为“在 $L$ 之前的 $a_i$ 都已经得到答案的基础上完成 $L$ ~ $R$ 的计算”。

  于是,我们可以写出下面的这个伪代码:

过程 solve(L,R)
|----mid←(L+R)/2
|----solve(L,mid)
|----FFT(a[L...mid]×k[1...R-L]→a[mid+1...R])
|----solve(mid+1,R)
过程结束

  其中在写代码的时候边界情况可能会有所不同。

  但是读者请注意,上面 $FFT()$ 里处理的不是右区间受到的全部贡献,只是完成了左区间对于右区间的贡献,事实上,一个 $a_i$ 会分成大约 $O(\log n)$ 次来贡献。

 

拆系数$FFT$和三模数$NTT$

毛啸2016的集训队论文有写,本节内容许多摘自他的论文,读者可以直接查阅他的论文。

  经典的 $FFT$ 的虽然很好用,但是在一些数据范围较大的题目之下,还是要挂。

  当两个长度为 $10^5$ 级别的序列卷积,模数为 $10^9$ 级别(不为 $NTT$ 模数),直接 $FFT$ 的话,每个数的结果大约在 $10^{23}$ 左右,超出了 $2^{64}$ ,一方面会使浮点数出现较大的误差,一方面你也不可能把他转成 $64$ 位整形然后取模,这个时候就要用下面的两种方法了。

拆系数$FFT$

  我们设 $M$ 为模数的大小,则取 $M_0=\left\lceil\sqrt{M}\ \right\rceil$ ,根据带余除法将所有的 $x$ 表示成 $x=k[x]M_0+b[x]$ 。其中 $k[x]$ 和 $b[x]$ 为整数。

  我们假设多项式 $A(x)$ 的系数序列为 $a_i$ ,多项式 $B(x)$ 的系数序列为 $b_i$ ,那么我们把 $k[a_i],b[a_i],k[b_i],b[b_i]$ 形成的四个序列两两卷积,卷积结果大约在 $10^{14}$ 级别,可以接受。并先取个模,然后乘上相应的系数,一起加到答案里面就可以了。这样要 $7$ 次 $(I)DFT$ 。通过 myy 论文里面讲的优化方法可以优化到 $4$ 次甚至 $3.5$ 次$DFT$。

三模数$NTT$

  我们找 $3$ 个大小在 $10^9$ 级别的 $NTT$ 模数。然后分别在这三个模数意义下求卷积结果,然后中国剩余定理合并即可。

  具体方法:我们假设模数分别是 $mod_0,mod_1,mod_2$ ,先合并前两个模数,也就是求出答案在模 $mod_0\times mod_1$ 意义下的值,然后用逆元将模 $mod_0\times mod_1\times mod_2$ 意义下的数,也就是答案表示成 $k\times mod_0\times mod_1+b$ 的形式,这个东西我们不必真正求出,我们只需要在模 $M$ 意义下求即可,这样只需要使用 $64$ 位整型即可。

  但这个需要 $9$ 次 $DFT$ 效率没有上面的那个快。

  (而且博主太菜了没写过,目前只写过 $9$ 次 $DFT$ 的拆系数 $FFT$ (截至2018-04-17))

套路与例题

以下例题的链接是详细题解,题解里面有题目链接

套路1 - 字符串匹配

  当我们从母串的某一个位置开始和模式串匹配的时候:

  首先,我们发现需要匹配的字符对的下标差会是定值,所以我们一般会把其中一个字符串进行翻转,因为翻转之后,需要匹配的字符对的下标之和为定值,这样才能满足卷积形式。(下面是翻转字符串之后,匹配连线的示意图)

  放例题:

  (注意,在此之后,我默认你已经能对是否翻转有敏感的认识)

BZOJ4053 两个串

题意

  给定两个字符串 S 和 T ,回答 T 在 S 中出现了几次,在哪些位置出现。注意 T 中可能有 ? 字符,可以匹配任何字符。串长 $\leq 10^5$ 

提示

  考虑到有通配符 $?$ ,我们不能直接 KMP 。

  但是我们可以通过构造卷积,通过判断卷积结果的某一位是否为 0 来确定某一个位置开始是否可以匹配。

  可以从相同字符值差为 0 以及通配符与其他字符永远匹配方面来考虑。

  第一道题,可以看看题解。

 

BZOJ4259 残缺的字符串

题意

  给你两个串,用其中一个来匹配另一个。问从母串的那些位置开始可以匹配模式串。注意有"*"可以匹配任何字符。

  串长 $\leq 3\times 10^5$ 。

提示

  和上一题唯一的区别就是两个串中都有通配符。基本上一样的。只是要稍微卡下时间和空间。

 

CodeForces 528D Fuzzy Search

题意

  给你两个串 $A,B(|A|\geq|B|)$ ,以及一个 $k$ 。

  其中 $A_i$ 与 $B_j$ 匹配的条件是 $A_{i-k\dots i+k}$ 中至少有一个与 $B_j$ 相同。

  问 $B$ 能在 $A$ 中匹配多少次。

  字符集: $\{'A','C','G','T'\}$ 。

  $|B|\leq|A|\leq 2\times 10^5,k\leq 2\times 10^5$

提示

  可以预处理每一个位置可以匹配到 $\{'A','C','G','T'\}$ 的哪些。

  然后,如果按照之前两题的套路来构造卷积,先不谈常数巨大,手工展开都很困难。

  注意到字符集非常小,我们可以考虑对于每一个字符分开考虑,构造卷积。

套路2 - 卷积形式变形

BZOJ3527 [ZJOI2014]力

题意

  给出长度为 $m$ 的序列 $q_{1..m}$ ,让你输出长度为 $m$ 的序列 $E_{1..m}$ 。

  其中:

$$E_i=\sum_{j=1}^{i-1}\frac{q_j}{(i-j)^2}-\sum_{j=i+1}^{m}\frac{q_j}{(i-j)^2}$$

提示

  将原式写成两个卷积式。其中一个很容易 $FFT$ ,另外一个可以通过翻转和更改求和指标等一系列推导变成 $FFT$ 擅长的卷积形式。

套路3 - 背包问题相关

  有时候,我们会碰到类似无限背包的问题。给定一些物品的体积,问用这些物品可以拼出某个范围内的哪些体积。

  构造多项式:如果有体积为 $i$ 的物品,则 $i$ 次项系数为 $1$ ,否则为 $0$ 。特别的,我们手工添加一个 $0$ 体积的物品。然后多项式平方一下,有值的位置所代表的体积就是可以通过至多 $2$ 个体积值相加得到。然后我们顺手把所有有值的改成 $1$ 。再平方,得到的是至多 $4$ 个体积值相加得到的体积。平方 $k$ 次,就能得到至多 $2^k$ 个物品体积相加可以得到的所有体积。复杂度 $O(n\log^2 n)$ 。其中 $n$ 为体积范围。

CodeForces 268E Ladies' Shop

题意

  首先,给你 $n$ 个数(并告诉你 $m$ ),分别为 $p_{1\dots n}$ 。

  让你求一个数的集合,满足:

    当且仅从这个数的集合中取数(可以重复)求和时(设得到的和为 $sum$ ),如果 $sum\leq m$ ,则数 $sum$ 在给你的 $n$ 个数之中。

  如果没有这种集合,输出 $NO$ 。

  否则,先输出 $YES$ ,然后输出这个集合最小时的元素个数,并输出集合中的所有元素。

  $1\leq n,m\leq 10^6,1\leq p_i\leq 10^6$

提示

  考虑思考本题的特殊性,在本题之前的小例子的基础上,舍去无用的运算。

套路4 - 分治$FFT$

BZOJ4836 [Lydsy1704月赛]二元运算

题意

  定义二元运算 $opt$ 满足

$$x\ opt\ y=\begin{cases}x+y & \text{$(x<y)$} \\ x-y & \text{$(x\geq y)$}\end{cases}$$

  现在给定一个长为 $n$ 的数列 $a$ 和一个长为 $m$ 的数列 $b$ ,接下来有 $q$ 次询问。每次询问给定一个数字 $c$ 你需要求出有多少对 $(i, j)$ 使得 $a_i\ opt\ b_j=c$ 。

提示

  在分治 $a_i$ 的值域的时候,左右区间内的数会满足左区间严格小于右区间,这是个很好的性质,会便于你按照上面的式子分类贡献, $FFT$ 优化。

 

CodeForces 553E Kyoya and Train

题意

  一个有 $n$ 个节点 $m$ 条边的有向图,每条边连接了 $a_i$ 和 $b_i$ ,花费为 $c_i$ 。

  每次经过某一条边就要花费该边的 $c_i$ 。

  第 $i$ 条边耗时为 $j$ 的概率为 $p_{i,j}$ 。

  现在你从 $1$ 开始走到 $n$ ,如果你在 $t$ 单位时间内(包括 $t$ )到了 $n$ ,不需要任何额外花费,否则你要额外花费 $x$ 。

  问你在最优策略下的期望花费最小为多少。

  (注意你每走一步都会根据当前情况制定最好的下一步)

提示

  本题是 myy 的论文题,思维含量较大。

  首先我告诉你 $O(mT\log^2 T)$ 的复杂度可以过。

  先考虑 $DP$ ,然后通过设一个期望贡献累加器,来化简 $DP$ 转移方程,并从中挖掘 $FFT$ 擅长的卷积形式,并通过分治 $FFT$ 优化。

杂题

BZOJ3160 万径人踪灭

题意

  给你一个只含 $a,b$ 的字符串,让你选择一个子序列,使得:

   $1.$ 位置和字符都关于某一条对称轴对称。

   $2.$ 不能是连续的一段。

  问原来的字符串中能找出多少个这样的子序列。答案对 $10^9+7$ 取模。

  串长 $\leq 10^5$ 。

提示

  先避开条件2考虑如何解题。考虑一个点为中心,在其两侧能互相匹配的字符对数。每一对可以互相匹配的都可以选择选或者不选。从中寻找卷积形式。

  对于不满足2的,显然是连续回文子串个数, $Manachar$ 裸题。

 

BZOJ4451 [Cerc2015]Frightful Formula

题意

  给你一个 $n\times n$ 矩阵的第一行和第一列,其余的数通过如下公式推出: 

$$f_{i,j}=a\cdot f_{i,j-1}+b\cdot f_{i-1,j}+c$$

  求 $f_{n,n}\mod (10^6+3)$ 。

提示

  考虑每一个格子各自对于 $f_{n,n}$ 的贡献。

  对于除了第一行和第一列的格子,性质相似,可以列出求和式子。再通过推导,寻找利于 $FFT$ 的卷积形式。

 

BZOJ4827 [Hnoi2017]礼物

题意

  有两个长为 $n$ 的序列 $x$ 和 $y$ ,序列 $x,y$ 的第 $i$ 项分别是 $x_i,y_i$ 。

  选择一个序列 $A$ ,现在你可以对它进行如下两种操作:

  $1.$ 得到一个和 $A$ 循环同构的序列 $A'$ 。

  $2.$ 给所有的 $A'_i$ 都加上 $c(c\in N^+)$ ,得到序列 $A''$ 。

  你进行上面两个操作之后,得到的序列分别为 $x'',y''$ (注意 $x,y$ 两个序列中至少有一个序列没有发生任何变化)

  序列 $x''$ 和 $y''$ 的差异值为

$$\sum_{i=1}^{n}(x''_i-y''_i)^2$$

  问差异值最小为多少。

提示

  考虑先写出一个一般的结果式子,然后略微展开,得到一些常数,一个关于 $c$ 的二次函数和一个卷积式。

  对于二次函数我们求一下最值即可。

  对于卷积式,我们考虑求其最值。先倍长某一个串,再翻转某一个串, $FFT$ 优化,计算出你需要的东西。

 

CodeForces 958F3 Lightsabers (hard)

题意

  有 $n$ 个球,球有 $m$ 种颜色,分别编号为 $1\cdots m$ ,现在让你从中拿 $k$ 个球,问拿到的球的颜色所构成的可重集合有多少种不同的可能。

  注意同种颜色球是等价的,但是两个颜色为 $x$ 的球不等价于一个。

  $1\leq n\leq 2\times 10^5,\ \ \ \ \ 1\leq m,k\leq n$。

提示

  一道比较新的题目,是我写这篇博文前几天的 $CodeForces$ 上的 $ACM$ 比赛题。

  考虑构造一些小的多项式,然后把他们全部乘起来得到最终的解。

  需要分治或者启发式合并优化。建议启发式合并。

 

CodeForces 623E Transforming Sequence

题意

  给定 $n,k$ 。

  让你构造序列 $a(0<a_i<2^k)$ ,满足 $b_i(b_i=a_1\ or\ a_2\ or\ \cdots\ or\ a_i)$ 严格单调递增。( $or$ 为按位或)

  问你方案总数。对 $10^9+7$ 取模。

  $n\leq 10^{18},k\leq 30000$

提示

  又是一道 myy 论文题。

  思维含量也挺大的。

  先考虑暴力 $DP$ ,然后考虑加大转移的步长,从已经得到的 $dp$ 值中状态转移得到新的 $dp$ 值。需要寻找你得到的加大步长后的 $dp$ 转移方程的利于 $FFT$ 的卷积形式,然后倍增 $FFT$ 优化。

参考文章与博客&鸣谢

(特别鸣谢)http://blog.miskcoo.com/2015/04/polynomial-multiplication-and-fast-fourier-transform#comment-37058

2016国家队候选队员论文 - 毛啸 - 再探快速傅里叶变换

《多项式导论》 - picks

https://oi.men.ci/fft-notes/

http://picks.logdown.com/posts/177631-fast-fourier-transform

http://picks.logdown.com/posts/247168-fast-fourier-transform-modulo-prime

后记

  写了好几天真累啊。感谢所有给我提供帮助的文章、博客,以及写它们的人,以及读完这篇学习笔记、看到这里的你。

  希望这篇博文能带给您帮助。

  由于博主学识短浅,如果您在阅读的过程中发现任何错误,麻烦您在百忙之中给我留言指出,谢谢。

  当然,多项式的运用远不止于此。关于多项式求逆、多项式除法、多项式开根、多项式 exp/ln 、多项式求导/求不定积分、牛顿迭代、泰勒展开等等,也许我会陆续推出关于这些算法学习笔记,敬请期待。

UPD(2018-04-18  15:00):自行验稿一遍,修改了约 10 处细节错误,比如空格没打或者同于打了等于这类的,以及一处 $DFT$ 写成了 $FFT$ ,均已修改。

UPD(2018-04-19  15:15):发现有一个题意概括里的细节错误,已经改正。

UPD(2018-04-20  20:06):感谢 Emoairx 指出,博主当时手残了,把拉格朗日插值法的复杂度写错了。现在已经修改。

UPD(2018-07-15  20:24):修正 3 处错误。

UPD(2018-09-23  18:45):补上了一个漏打的字

posted @ 2018-04-14 14:52  zzd233  阅读(9375)  评论(13编辑  收藏  举报