• 多项式学习笔记(一) 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) =displaystylesum_{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) imes B(x) = C(x)) ,那么 (C_i = displaystylesum_{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} imes w_{n}^{1} = (cos{kover n}2π,sin{nover k}2π) * (cos{1over n}2π,sin{1over n}2π))

    (=(cos{kover n}2π imes cos{1over n}2π-sin{nover k}2π imes sin{1over n} 2π,cos{kover n}2π imes sin{1over n}2π-sin{kover n}2π imes cos{1over n}2π)

    (=(cos({kover n}+{1over n})2π,sin({kover n}+{1over 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*xover{n*x}}2π,sin{k*xover{n*x}}2π))

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

    (=w_{n}^{k})

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

    证明:

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

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

    数学证明:

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

    (=-(-cos{kover n}2π,-sin{kover n}2π))

    (=(cos({kover n}2π,sin{kover 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 = displaystylesum_{i=0}^{n-1}y_i * (w_{n}^{-k})^i)

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

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

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

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

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

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

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

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

    (displaystylesum_{i=0}^{n-1}(w_n^{j-k})^i = {(w_{n}^{j-k})^n -1over w_{n}^{j-k}-1} = {(w_{n}^{n})^{j-k}-1over w_n^{j-k}-1} = {1^{j-k}-1over 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))

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

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

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

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

    (A(w_{n}^{k+{nover 2}}) = A1(w_{n}^{2k+n})+w_{n}^{k+{nover 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_{nover 2}^{k})-w_{n}^{k}A2(w_{nover 2}^k))

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

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

    复杂度 (T(n) = 2*T({nover 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("
    ");
    	return 0; 
    }
    
  • 相关阅读:
    C#几个经常犯错误汇总
    vs2010密钥
    SQL数据库基础知识用sql语句创建数据库
    空值显示表格线条(border)
    vs2008常用快捷方式
    汉字与十六进制之间的相互转换
    asp.net中常用信息验证总结
    GridView中全选和反选
    Nonreentrant C# timer
    GC.Collect
  • 原文地址:https://www.cnblogs.com/genshy/p/14161564.html
Copyright © 2020-2023  润新知