【算法笔记 - 1】多项式乘法 —— FFT

目录


@0 - 参考资料@

Miskcoo's Space 的讲解

@1 - 一些概念@

多项式的系数表示法:形如 \(A(x)=a_0+a_1x+...+a_{n-1}x^{n-1}\)

多项式的点值表示法:对于 n-1 次多项式 \(A(x)\),我们选取 n 个不同的值 \(x_0, x_1, ... , x_{n-1}\) 代入 \(A(x)\),得到 \(y_i=A(x_i)\)。则 \((x_0, y_0),...,(x_{n-1},y_{n-1})\) 称为多项式的点值表示。
把多项式系数当作 n 个变量,n 个点当作 n 个线性方程,可以用高斯消元来求得唯一解。因此,我们可以用这 n 个点唯一表示 A(x) 。
注意,一个多项式的点值表示法并不是唯一的。

如果用点值表示法的多项式作乘法,可以直接把纵坐标相乘,在 O(n) 的时间实现多项式乘法。

FFT(快速傅里叶变换)可以实现 O(nlog n) 的 点值表示 与 系数表示 之间的转换。

一个解释多项式乘法原理的图:
多项式乘法图

复数:复数简单来说就是 \(a + bi\),其中\(i^2=-1\)。可以发现复数 \(a + bi\) 与二维平面上的向量 \((a, b)\) 一一对应。
复数的乘法可以直接(a + bi)(c + di)展开相乘。但是几何上复数乘法还有另一种解释:
复数
这样定义下,复数的乘法为模长相乘,幅角相加。

单位根:定义 n 次单位根为使得 \(z^n=1\) 成立的复数 \(z\)。一共 n 个,在单位圆上且 n 等分单位圆。
可以发现 n 次单位根模长都为 1,幅角依次为\(0*\frac{2\pi}{n},1*\frac{2\pi}{n},...,(n-1)*\frac{2\pi}{n}\)
我们记 n 次单位根依次为\(w_n^0,w_n^1,...w_n^{n-1}\)
有以下几个性质:
(1)\(w_{n}^{i}*w_{n}^{j}=w_{n}^{i+j}\)
(2)\(w_{dn}^{dk}=w_n^k\),有点类似于分数的约分。
(3)\(w_{2n}^k=-w_{2n}^{k+n}\)
以上几个性质都可以从幅角的方面去理解。

@2 - 傅里叶正变换@

FFT 的正变换实现,是基于对多项式进行奇偶项分开递归再合并的分治进行的。
对于 n-1 次多项式,我们选择插入 n 次单位根求出其点值表达式。

记多项式\(A(x)=a_0+a_1x+a_2x^2+...+a_{n-1}x^{n-1}\)
再记\(A_o(x)=a_1+a_3x+a_5x^2+...\)
再记\(A_e(x)=a_0+a_2x+a_4x^2+...\)
\(A(x)=x*A_o(x^2)+A_e(x^2)\)

扫描二维码关注公众号,回复: 4617305 查看本文章

\(n = 2*p\)。则有:
\(A(w_n^k)=w_n^k*A_o[(w_{n/2}^{k/2})^2]+A_e[(w_{n/2}^{k/2})^2]=w_n^k*A_o(w_p^k)+A_e(w_p^k)\)
\(A(w_n^{k+p})=w_n^{k+p}*A_o(w_p^{k+p})+A_e(w_p^{k+p})=-w_n^k*A_o(w_p^k)+A_e(w_p^k)\)

在已知 \(A_o(w_p^k)\)\(A_e(w_p^k)\) 的前提下,可以 O(1) 算出 \(A(w_n^k)\)\(A(w_n^{k+p})\)

因此,假如我们递归求解 \(A_o(x),A_e(x)\) 两个多项式 p 次单位根的插值,就可以在O(n)的时间内算出 \(A(x)\) n 次单位根的插值。

时间复杂度是经典的 \(T(n)=2*T(n/2)+O(n)=O(n\log n)\)

@3 - 傅里叶逆变换@

观察我们刚刚的插值过程,实际上就是进行了如下的矩阵乘法。
\(\begin{bmatrix} (w_n^0)^0 & (w_n^0)^1 & \cdots & (w_n^0)^{n-1} \\ (w_n^1)^0 & (w_n^1)^1 & \cdots & (w_n^1)^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (w_n^{n-1})^0 & (w_n^{n-1})^1 & \cdots & (w_n^{n-1})^{n-1} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \begin{bmatrix} A(w_n^0) \\ A(w_n^1) \\ \vdots \\ A(w_n^{n-1}) \end{bmatrix}\)

我们记上面的系数矩阵为 \(V\)
对于下面定义的 \(D\)
\(D = \begin{bmatrix} (w_n^{-0})^0 & (w_n^{-0})^1 & \cdots & (w_n^{-0})^{n-1} \\ (w_n^{-1})^0 & (w_n^{-1})^1 & \cdots & (w_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (w_n^{-(n-1)})^0 & (w_n^{-(n-1)})^1 & \cdots & (w_n^{-(n-1)})^{n-1} \end{bmatrix}\)

考虑 \(D*V\)的结果:
\((D*V)_{ij}=\sum_{k=0}^{k<n}d_{ik}*v_{kj}=\sum_{k=0}^{k<n}w_n^{-ik}*w_{n}^{kj}=\sum_{k=0}^{k<n}w_n^{k(j-i)}\)
当 i = j 时,\((D*V)_{ij}=n\)
当 i ≠ j 时,\((D*V)_{ij}=1+w_n^{j-i}+(w_n^{j-i})^2+...=\frac{1-(w_n^{j-i})^n}{1-w_n^{j-i}}\)=0;
【根据定义,n 次单位根的 n 次方都等于 1】

所以:\(\frac1n*D=V^{-1}\)
因此将这个结果代入最上面那个公式里面,有:
\(\begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix} = \frac1n \begin{bmatrix} (w_n^{-0})^0 & (w_n^{-0})^1 & \cdots & (w_n^{-0})^{n-1} \\ (w_n^{-1})^0 & (w_n^{-1})^1 & \cdots & (w_n^{-1})^{n-1} \\ \vdots & \vdots & \ddots & \vdots \\ (w_n^{-(n-1)})^0 & (w_n^{-(n-1)})^1 & \cdots & (w_n^{-(n-1)})^{n-1} \end{bmatrix}\begin{bmatrix} A(w_n^0) \\ A(w_n^1) \\ \vdots \\ A(w_n^{n-1}) \end{bmatrix}\)

“这样,逆变换 就相当于把 正变换 过程中的 \(w_n^k\) 换成 \(w_n^{-k}\),之后结果除以 n 就可以了。”——摘自某博客。

……
还是有点难理解。比如为什么我们不直接把\(w_n^k\) 换成 \(\frac1n*w_n^{-k}\) 算了。
实际上,因为\(w_n^{-k}=w_n^{n-k}\),也就是说它 TM 还是一个 n 次单位根。所以我们插值还是正常的该怎么插怎么插。如果换成 \(\frac1n*w_n^{-k}\) 它就不是一个单位根,以上性质就不满足了。

@4 - 迭代实现 FFT@

递归版本的 FFT 虽好,可奈何常数太大。
我们考虑怎么迭代实现 FFT。观察奇偶分组后各数的位置。
FFT迭代实现图
原序列:0,1,2,3,4,5,6,7。
终序列:0,4,2,6,1,5,3,7。
转换为二进制再来看看。
原序列:000,001,010,011,100,101,110,111。
终序列:000,100,010,110,001,101,011,111。
可以发现终序列是原序列每个元素的翻转。
于是我们可以先把要变换的系数排在相邻位置,从下往上迭代。

这个二进制翻转过程可以自己脑补方法,只要保证时间复杂度O(nlog n),代码简洁就可以了。
在这里给出一个参考的方法:
我们对于每个 i,假设已知 i-1 的翻转为 j。考虑不进行翻转的二进制加法怎么进行:从最低位开始,找到第一个为 0 的二进制位,将它之前的 1 变为 0,将它自己变为 1。因此我们可以从 j 的最高位开始,倒过来进行这个过程。

@5 - 参考代码实现@

本代码为 uoj#34 的AC代码。在代码中有些细节可以关注一下。

#include<cmath>
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 400000;
const double PI = acos(-1);
struct complex{
    double r, i;
    complex(double _r=0, double _i=0):r(_r), i(_i){}
};//C++ 有自带的复数模板库,但很显然我并不会。
typedef complex cplx;
cplx operator +(cplx a, cplx b){return cplx(a.r+b.r, a.i+b.i);}
cplx operator -(cplx a, cplx b){return cplx(a.r-b.r, a.i-b.i);}
cplx operator *(cplx a, cplx b){return cplx(a.r*b.r-a.i*b.i, a.r*b.i+b.r*a.i);}
void FFT(cplx *A, int n, int type) {
    for(int i=0,j=0;i<n;i++) {
        if( i < j ) swap(A[i], A[j]);
        for(int k=(n>>1);(j^=k)<k;k>>=1);//这个地方读不懂就背吧。。。
    }
    for(int s=2;s<=n;s<<=1) {
        int t = (s>>1);
        cplx u = cplx(cos(type*PI/t), sin(type*PI/t));
//这个地方需要注意一点:如果题目中需要反复用到 FFT,则可以预处理出所有单位根以减小常数。
        for(int i=0;i<n;i+=s) {
            cplx r = cplx(1, 0);
            for(int j=0;j<t;j++,r=r*u) {
                cplx e = A[i+j], o = A[i+j+t];
                A[i+j] = e + r*o; A[i+j+t] = e - r*o;
            }
        }
    }
}
cplx A[MAXN + 5], B[MAXN + 5], C[MAXN + 5];
int main() {
    int n, m;
    scanf("%d%d", &n, &m); n++, m++;
    for(int i=0;i<n;i++)
        scanf("%lf", &A[i].r);
    for(int i=0;i<m;i++)
        scanf("%lf", &B[i].r);
    int len = 1;
    while( len < (n+m-1) ) len <<= 1;
//此处重点:由于我们每一次都要奇偶分组,所以长度必须为2的整数次幂,高位补0就好了。
    FFT(A, len, 1); FFT(B, len, 1);
    for(int i=0;i<len;i++)
        C[i] = A[i] * B[i];
    FFT(C, len, -1);
    for(int i=0;i<n+m-1;i++)
        printf("%d ", int(round(C[i].r/len)));//记得,一定要记得除以len。
}

@6 - 快速数论变换 NTT@

实际上这可以算是 FFT 的一个优化。
FFT虽然跑得快,但是因为是浮点数运算,终究还是有 精度、常数 等问题。
然而问题来了:我们多项式乘法都是整数在那里搞来搞去,为什么一定要扯到浮点数。是否存在一个在模意义下的,只使用整数的方法?

想一想我们用了哪些单位根的性质:

(1)\(w_{n}^{i}*w_{n}^{j}=w_{n}^{i+j}\)
(2)\(w_{dn}^{dk}=w_n^k\)
(3)\(w_{2n}^k=-w_{2n}^{k+n}\)
(4) n 个单位根互不相同,且 \(w_n^0=1\)

我们能否找到一个数,在模意义下也满足这些性质?
引入原根的概念:对于素数 p,p 的原根 G 定义为使得 \(G^0,G^1,...,G^{p−2}(\mod p)\) 互不相同的数。
再定义 \(g_n^k = (G^{\frac{p-1}{n}})^k\)。验证一下这个东西是否满足单位根的以上性质。
(1),由幂的运算立即可得。
(2),由幂的运算立即可得。
(3),\(g_{2n}^{k+n}=(G^{\frac{p-1}{2n}})^{k+n}=(G^{\frac{p-1}{2n}})^k*(G^{\frac{p-1}{2n}})^n=G^{\frac{p-1}{2}}*g_{2n}^k=-g_{2n}^k(\mod p)\)
【因为\(G^{p-1}=1(\mod p)\)且由原根定义\(G^{\frac{p-1}{2}}\not=G^{p-1}(\mod p)\),故\(G^{\frac{p-1}{2}}=-1(\mod p)\)
(4),由原根的定义立即可得。

所以我们就可以搞 NTT 了。直接把代码中涉及单位根的换成原根即可。

然而,可以发现 NTT 适用的模数 m 十分有限。它应该满足以下性质:
(1)令 \(m = 2^p*k+1\),k 为奇数,则多项式长度必须 \(n \le 2^p\)
(2)方便记忆,方便记忆,与方便记忆。

这里有一些合适的模数【来源:miskcoo】。

猜你喜欢

转载自www.cnblogs.com/Tiw-Air-OAO/p/10162034.html