多项式学习笔记(一) FFT

1.多项式

定义 \(F(x)\) 表示一个 \(n-1\) 次的多项式(其实你可以把多项式理解为方程)。

\(F(x) = a_0x^0 + a_1x^1 + a_2x^2+...a_{n-1}x^{n-1}\) ,即 \(F(x) =\displaystyle\sum_{i=0}^{n-1}a^ix^i\)

这也就是我们经常用的系数表示法(初中应该都学过吧)。

2.点值表示法

在介绍 \(FFT\) 之前,有必要接触一下这种方法。

首先我们可以知道 \(n\) 个点可以确定一个 \(n-1\) 次的多项式,因为你可以通过各种消元得到每一次的系数。

\(F(x) = x^2+3x+1\) . 我们就可以用点值表示法表示为 \((0,1),(1,5),(-1,-1)\).

3.多项式乘法

其实多项式乘法就是卷积的形式。

假如我们有两个多项式 \(A(x)\)\(B(x)\), 其中 \(A(x) = a_0x^0+a_1x^1+a_2x^2....a_{n-1}x^{n-1}\)\(B(x) = b_0x^0+b_1x^1+b_2x^2....b_{m-1}x^{m-1}\)

显然如果 \(A(x) \times B(x) = C(x)\) ,那么 \(C_i = \displaystyle\sum_{i=0}^{i}a_ib_{i-j}\).

这样直接乘的复杂度是 \(O(n^2)\) 的,下面要讲的 \(FFT\) 则可以使他变为 \(O(nlogn)\)

4.FFT

这个就是我们今天的主角,它的全称叫 快速离散傅里叶变化,据说傅里叶大神在计算机发明的100多年前就发明了这个算法。

下面我们来看看他是具体怎么操作的。

假如说我们已经知道了 \(A(x)\)\(B(x)\)\(x_0\) 处的取值,那么 \(C(x)\)\(x_0\) 处的取值直接把两个数相乘就可以得到。

间接地我们就知道了 \(C(x)\) 的点值表示法。

来后整个 \(FFT\) 的流程我们就大致知道了:

  • 求值,分别求出多项式 \(A(x),B(x)\) 的点值表示法
  • 相乘,把 \(A(x),B(x)\) 的点值乘起来,得到 \(C(x)\) 的点值表示法 。
  • 插值,把 \(C(x)\) 的点值表示法转化为系数表示法。

中间第二部相乘,显然可以做到 \(O(n)\) 的来写。第一步朴素算法就是找 \(n\) 个不同的值分别代入方程中,复杂度为 \(O(n^2)\) 的,第三部复杂度就更槽糕了,直接高斯消元的话复杂度直接变为 \(O(n^3)\) 的,这比直接相乘的暴力还要慢,显然还需要继续优化。

运用点值表示法,我们可以任意选 \(n\) 个数代进去,这启发我们从这里入手开始优化,那么傅里叶大神也注意到了这一点,把复数和单位根引进了这个方程中。

5.单位根及其性质

在了解把复数和单位根引入这个方程有什么用前,要先了解一下,单位根的性质。

首先,单位根所在的点就是把单位圆平均分为 \(n\) 份的分割点,也就是一个向量。

我们一般从 \((0,1)\) 开始逆时针开始标号得到 \(w_{n}^{0},w_{n}^{1}...w_{n}^{n-1}\),,

例如八次单位根

单位根有如下几条性质:

性质1: \(w_{n}^{k} = (w_{n}^{1})^k\)

证明:

\(w_{n}^{k} \times w_{n}^{1} = (\cos{k\over n}2π,\sin{n\over k}2π) * (\cos{1\over n}2π,\sin{1\over n}2π)\)

\(=(\cos{k\over n}2π\times \cos{1\over n}2π-\sin{n\over k}2π\times \sin{1\over n} 2π,\cos{k\over n}2π\times \sin{1\over n}2π-\sin{k\over n}2π\times cos{1\over n}2π\)

\(=(\cos({k\over n}+{1\over n})2π,\sin({k\over n}+{1\over n})2π)\)

\(=(\cos{{k+1}\over n}2π,\sin{{k+1}\over n}2π)\)

\(= w_{n}^{k+1}\)

至于第二步怎么转化过来的,具体可以看一下 和角公式

性质2: \((w_{n}^{x})^{y} = w_{n}^{x*y}\)

证明:

首先我们有 \((a^x)^y = a^{x*y}\) ,

然后就有 \((w_{n}^{x})^y = ((w_n^{1})^x)^y = (w_{n}^{1})^{x*y} = w_{n}^{x*y}\).

性质3\(w_{n*x}^{k*x} = w_{n}^{k}\)

证明:

\(w_{n*x}^{k*x} = (\cos{k*x\over{n*x}}2π,\sin{k*x\over{n*x}}2π)\)

\(=\cos_{n}^{k}2π,\sin_{n}^{k}2π\) (约分大法好)

\(=w_{n}^{k}\)

性质4: 如果 \(n\) 为偶数,则 \(w_{n}^{k} = -w_{n}^{k+{n\over 2}}\)

证明:

你观察一下上面的那个图就会发现, \(w_n^{k+{n\over 2}}\) 所表示的向量其实是 \(w_n^{k}\) 这个向量旋转 180 度之后得到的。

然后仔细观察一下他们好像关于原点对称,那么自然满足 \(w_{n}^{k} = -w_{n}^{k+{n\over 2}}\).

数学证明:

\(-w_{n}^{k+{n\over 2}} = -(\cos({k+{n\over 2}\over n})2π,\sin({{k+{n\over 2}}\over {n}}2π)\)

\(=-(-\cos{k\over n}2π,-\sin{k\over n}2π)\)

\(=(cos({k\over n}2π,\sin{k\over n}2π)\)

​ $=w_{n}^{k} $

诱导公式nb, \(\cos(x+π) = -\cos x\), \(sin(x+π) = -sin(x+π)\)

6.插值优化

既然单位根有那么多的性质,那么把他代入方程会产生神马化学反应呢?

第一点就是比较方便插值。

首先 \(F(x)\) 的离散傅里叶变换指把 \(w_n^{0},w_{n}^{1}....w_{n}^{n-1}\) 作为多项式(方程)的 \(x\) 代入得到 \((y_0,y_1...y_{n-1})\) ,实际上他是一个插值的过程。

然后现在有一个结论:

对一个多项式进行离散傅里叶变换得到的 \((y_0,y_1...y_{n-1})\) 作为系数组成一个新的多项式 \(B(x)\),然后把 \(n\) 个单位根 \(w_n^{0},w_{n}^{1}....w_{n}^{n-1}\) 取倒数代入得到 \((z_0,z_1....z_{n-1})\) , 那么 \(A(x)\)\(i\) 项的系数就是 \(z_{i}\) 除以 \(n\) 的结果。

具体来说就是:

\(A(x) = a_0x^0+a_1x^1+a_2x^2...a_{n-1}x^{n-1}\)

\(w_n^{0},w_{n}^{1}....w_{n}^{n-1}\) 分别作为 \(x\) 代入求值得到 \((y_0,y_1...y_{n-1})\)

\(B(x) = y_0x^0 + y_1x^1+y_2x^2+y_{n-1}x^{n-1}\)

然后再把 \((w_{n}^{-1},w_{n}^{-2}...w_{n}^{-(n-1)})\) 作为 \(x\) 代入求值,得到 \((z_0,z_1.....z_{n-1})\)

最后 \(z_k = a_{k}*n\)

证明:

\(z_k = \displaystyle\sum_{i=0}^{n-1}y_i * (w_{n}^{-k})^i\)

\(=\displaystyle\sum_{i=0}^{n-1}(w_{n}^{-k})^i(\sum_{j=0}^{n-1}a_j * (w_{n}^{i})^j )\)

\(=\displaystyle\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j*(w_{n}^{i})^j * (w_{n}^{-k})^i\)

\(=\displaystyle\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j*w_n^{i*j} * w_n^{-k*i}\)

\(=\displaystyle\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j*w_{n}^{i*(j-k)}\)

\(=\displaystyle\sum_{j=0}^{n-1}a_j \sum_{i=0}^{n-1}(w_n^{j-k})^i\)

对于 \(\displaystyle\sum_{i=0}^{n-1}(w_n^{j-k})^i\) 我们发现,当 \(j ==k\) 的时候,这个值等于 \(n\) .

其他情况下这个值都为 \(0\) ,这个需要用等比序列求和来做。

首先,把求和项展开可以发现是一个公比为 \(w_n^{j-k}\) 的一个等比序列,利用求和公式可得:

\(\displaystyle\sum_{i=0}^{n-1}(w_n^{j-k})^i = {(w_{n}^{j-k})^n -1\over w_{n}^{j-k}-1} = {(w_{n}^{n})^{j-k}-1\over w_n^{j-k}-1} = {1^{j-k}-1\over w_n^{j-k}-1} = 0\)

因此 \(z_k = a_k * n\).

所以,我们利用离散傅里叶变换,把插值就变为了一次求值的过程,现在主要是解决求值的问题。

7.求值优化

首先我们定义一个多项式 \(A(x) = a_0x^0+a^1x^1+a^2x^2....a_{n-1}x^{n-1}\), 然后要求的是把 \(w_{n}^{0},w_{n}^{1}....w_{n}^{n-1}\) 代入后的结果。

这个到底怎么做的,我们现在引进快速傅里叶变换。

它主要把次数按奇偶项分类进行处理。

比如: \(A(x) = (a_0x^0+a_2x^{2}+a_4x^4.....) + (a_1x^1+a_3x^3+a^5x^5......)\)

\(A1(x) = a_0x^0+a^2x^1+a_4x^2.....\) , \(A2(x) = a_1x^0+a_3x^1+a_5x^2...\)

显然 \(A(x) = A1(x^2) + xA2(x^2)\)

然后我们尝试把单位根代入

如果 \(这个值 < {n\over 2}\) ,我们直接把 \(k\) 代入得:

\(A(w_n^{k}) = A1(w_n^{2k}) + w_n^{k}A2(w_n^{2k}) = A1(w_{n\over 2}^{k}) + w_{n}^{k}A2(w_{n\over 2}^{k}))\)

如果 $这个值\geq {n\over 2} $,我们把 \(w_{n}^{k+{n\over 2}}\) 代入可得:

\(A(w_{n}^{k+{n\over 2}}) = A1(w_{n}^{2k+n})+w_{n}^{k+{n\over 2}}A2(w_{n}^{2k+n}) = A1(w_{n}^{2k}*w_{n}^{n}) - w_{n}^{k}A2(w_{n}^{2k}*w_{n}^{n}) = A1(w_{n\over 2}^{k})-w_{n}^{k}A2(w_{n\over 2}^k)\)

假如你知道 \(A1(x)\)\(A2(x)\) 代入 \(w_{n\over 2}^{0},w_{n\over 2}^{1}....w_{n\over 2}^{{n\over 2}-1}\) 的结果,那么 \(A(x)\) 代入 \(w_{n}^{0},w_{n}^1....w_{n}^{n-1}\) 的值我们就可以 \(O(n)\) 的得出来了。

我们可以一直递归下去,直到 \(n=0\) 时,柿子的值就是 \(w_n^0 \times 系数\) ,就可以直接 \(return\) 了。

复杂度 \(T(n) = 2*T({n\over 2})+O(n) = nlog n\)

下面给出 FFT 递归的实现版本

void FFT(complex *a,int len,int flag)
{
	if(len == 1) return;//只有一次项的时候停止递归
	int A1[N],A2[N];
	for(int i = 0; i < len; i += 2)//按奇偶的奇偶分类递归
	{
		A1[i>>1] = a[i];
		A2[i>>1] = a[i+1]; 
	}
	FFT(A1,len>>1,flag); FFT(A2,len>>1,flag);//递归计算 A1(x),A2(x)
	complex wn = {cos(Pi/len),flag * sin(Pi/len)};//单位根
	for(int i = 0; i < len>>1; i++)
	{
		complex u = A1[i];
		complex v = w * A2[i];
		a[i] = u + v;
		a[i+(len>>1)] = u-v;
		w = w * wn;//下一个单位根
	}
}

8.递归转迭代

上面那么伪代码是不可以通过模板的,主要是递归的时候计算耗费了大量的常数。

一个比较好的优化方法就是递归转迭代。

有位大神发现 FFT 递归的时候有个性质,就是对于 \(a_x\) 他最后的位置就是把 \(x\) 的二进制位翻转的位置。

比如 \(4(100)->2(001)\) ,可以结合图理解一下:

这样我们可以 \(O(n)\) 的预处理处每个 \(ax\) 最后的系数,然后就可以借助非递归快速实现了。

总代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 5e6+10;
const double Pi = acos(-1.0);
int n,m,len,tim,Rev[N];
inline int read()
{
	int s = 0,w = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-')w = -1; ch = getchar();}
	while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
	return s * w;
}
struct complex//定义一个复数
{
	double x,y;
//	complex(double a,double b){x = a, y = b;}
}a[N],b[N];
complex operator + (complex a, complex b){return (complex){a.x+b.x,a.y+b.y};}//重载复数的各种操作
complex operator - (complex a, complex b){return (complex){a.x-b.x,a.y-b.y};}
complex operator * (complex a, complex b){return (complex){a.x*b.x-a.y*b.y,a.x*b.y+a.y*b.x};}
void FFT(complex *a,int flag)
{
	for(int i = 0; i < len; i++) 
	{
		if(i < Rev[i]) swap(a[i],a[Rev[i]]);
	}
	for(int h = 1; h < len; h <<= 1)//从下往上开始合并,枚举这次合并的区间长度的一半是多少
	{
		complex wn = complex{cos(Pi/h),flag*sin(Pi/h)};//单位根
		for(int j = 0; j < len; j += (h<<1))//枚举这一层每一组的起始位置
		{
			complex w = complex{1,0};
			for(int k = 0; k < h; k++)//处理每一组的值
			{
				complex x = a[j+k];
				complex t = w * a[j+k+h];
				a[j+k] = x+t;
				a[j+k+h] = x-t;
				w = w * wn;
			}
		}
	}
}
int main()
{
	n = read(); m = read();
	for(int i = 0; i <= n; i++) a[i].x = read();
	for(int i = 0; i <= m; i++) b[i].x = read();
	len = 1, tim = 0;
	while(len <= n+m) len <<= 1, tim++;
	for(int i = 0; i < len; i++) Rev[i] = (Rev[i>>1]>>1) | ((i&1)<<(tim-1));//预处理每个系数最后的位置,只可意会不可言传
	FFT(a,1); FFT(b,1);
	for(int i = 0; i < len; i++) a[i] = a[i] * b[i];
	FFT(a,-1);
	for(int i = 0; i <= n+m; i++) printf("%d ",(int) (a[i].x/len+0.5));
	printf("\n");
	return 0; 
}
posted @ 2020-12-19 22:10  genshy  阅读(260)  评论(0编辑  收藏  举报