@总结 - 1@ 多项式乘法 —— FFT

目录

  • @0 - 参考资料@
  • @1 - 一些概念@
  • @2 - 傅里叶正变换@
  • @3 - 傅里叶逆变换@
  • @4 - 迭代实现 FFT@
  • @5 - 参考代码实现@
  • @6 - 快速数论变换 NTT@
  • @7 - 任意模数 NTT@
    • @三模数 NTT@
    • @拆系数 FFT@(留坑待填)
  • @8 - 例题与应用@
    • @分治 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) 的 点值表示 与 系数表示 之间的转换。

一个解释多项式乘法原理的图:
@总结 - 1@ 多项式乘法 —— FFT_第1张图片

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

单位根:定义 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)\)

\(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
当 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。观察奇偶分组后各数的位置。
@总结 - 1@ 多项式乘法 —— FFT_第3张图片
原序列: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
#include
#include
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>1);(j^=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

@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】。

NTT 参考代码,一样是 uoj 的那道题。

#include
#include
using namespace std;
const int MOD = 998244353;
const int MAXN = 400000;
const int G = 3;
int pow_mod(int b, int p) {
    int ret = 1;
    while( p ) {
        if( p & 1 ) ret = 1LL*ret*b%MOD;
        b = 1LL*b*b%MOD;
        p >>= 1;
    }
    return ret;
}
void NTT(int *A, int n, int type) {
    for(int i=0,j=0;i j ) swap(A[i], A[j]);
        for(int l=(n>>1);(j^=l)>=1);
    }
    for(int s=2;s<=n;s<<=1) {
        int t = (s>>1), u = (type == 1) ? pow_mod(G, (MOD-1)/s) : pow_mod(G, (MOD-1)-(MOD-1)/s);
        for(int i=0;i

@7 - 任意模数 NTT@

假如题目中规定了模数怎么办?还卡 FFT 的精度怎么办?
有两种方法:

@三模数 NTT@

我们可以选取三个适用于 NTT 的模数 M1,M2,M3 进行 NTT,用中国剩余定理合并得到 x mod (M1*M2*M3) 的值。只要保证 x < M1*M2*M3 就可以直接输出这个值。
之所以是三模数,因为用三个大小在 10^9 左右模数对于大部分题目来说就足够了。

但是 M1*M2*M3 可能非常大怎么办呢?难不成我还要写高精度?其实也可以。
我们列出同余方程组:
\[\begin{cases} x \equiv a_1&\mod m_1\\ x \equiv a_2&\mod m_2\\ x \equiv a_3&\mod m_3\\ \end{cases}\]
先中国剩余定理(这个不会……我真的帮不了 qwq)合并前两个方程组:
\[ \begin{cases} x \equiv A&\mod M\\ x \equiv a_3&\mod m_3\\ \end{cases} \]
其中 M = m1*m2 < 10^18。

然后将第一个方程变形得到 \(x = kM + A\) 代入第二个方程:
\[ kM+A \equiv a_3\mod m_3\\ k \equiv (a_3-A)*M^{-1} \mod m_3\\ \]
令 $Q = (a_3-A)*M^{-1} $,则 \(k = Pm_3 + Q\)

再将上式代入回 \(x = kM + A\),得 \(x = (Pm_3 + Q)M+ A = Pm_3M+QM+A\)

又因为 \(M = m_1m_2\),所以 \(x = Pm_1m_2m_3 + QM + A\)

也就是说 \(x \equiv QM + A \mod m_1m_2m_3\)

然后……然后就这样啊。
一份 luoguP4243 的 AC 代码:

#include
#include
using namespace std;
typedef long long ll;
const ll G = 3;
const int MAXN = 400000;
const ll MOD[3] = {469762049, 998244353, 1004535809};
//模数记不住怎么办?身为一名 OIer 啊,就要做自己最擅长的事情(指暴力打表)。
ll pow_mod(ll b, ll p, ll mod) {
    ll ret = 1;
    while( p ) {
        if( p & 1 ) ret = ret*b%mod;
        b = b*b%mod;
        p >>= 1;
    }
    return ret;
}
ll mul_mod(ll a, ll b, ll mod) {
    ll ret = 0;
    while( a ) {
        if( a & 1 ) ret = (ret + b)%mod;
        b = (b + b)%mod;
        a >>= 1;
    }
    return ret;
}
ll inv[3][3], M, k1, k2, Inv;
void init() {
    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if( i != j ) inv[i][j] = pow_mod(MOD[i], MOD[j]-2, MOD[j]);
    M = MOD[0]*MOD[1];
    k1 = mul_mod(MOD[1], inv[1][0], M);
    k2 = mul_mod(MOD[0], inv[0][1], M);
    Inv = inv[0][2]*inv[1][2]%MOD[2];
}
ll CRT(ll a1, ll a2, ll a3, ll mod) {
    ll A = (mul_mod(a1, k1, M) + mul_mod(a2, k2, M))%M;
    ll K = (a3 + MOD[2] - A%MOD[2])%MOD[2]*Inv%MOD[2];
    return ((M%mod)*K%mod + A)%mod;
}
//27 ~ 40 行,三模数 NTT 的精华 owo 
ll f[3][MAXN + 5], g[3][MAXN + 5];
void ntt(ll *A, int n, int m, int type) {
    for(int i=0, j=0;i>1);(j^=l)>=1);
    }
    for(int s=2;s<=n;s<<=1) {
        int t = (s>>1);
        ll u = (type == -1) ? pow_mod(G, (MOD[m]-1)/s, MOD[m]) : pow_mod(G, (MOD[m]-1) - (MOD[m]-1)/s, MOD[m]);
        for(int i=0;i

@拆系数 FFT@(留坑待填)

不会 QAQ 我太弱了 QAQ。

@8 - 例题与应用@

@分治 FFT@

算是 FFT 的一个简单的扩展吧。。。
其问题大致可以描述为:

\(C\)\(A,B\) 的卷积,其中 \(B\) 是一开始就已知的。依次给出 \(a_0, a_1,\dots\) 的值,当给出 \(a_i\) 的值时,需要立即算出 \(c_i\) 的值。

解决方法就是使用 cdq 分治 + FFT。如果你不知道 cdq 分治是什么也没关系,只需要知道它是个分治就 OK。

假如我们已知 \([le, mid]\) 内所有的 \(A[i]\),则 \([le, mid]\)\([mid+1, ri]\) 的贡献为:
\[C[i] += \sum_{le \le j\le mid}^{j+k=i}A[j]*B[k](mid+1 \le i \le ri)\]
可以求出 k 的范围为 1 <= k <= ri-le。这是一个长度为 ri - le 的卷积,可以用 FFT 来优化。

我们递归时先递归左边,再卷积计算左边那一半对右边那一半的贡献,最后递归右边。
注意分治的区间长度不一定要是 2 的幂。

@例题@ : @codeforces - 553E@ Kyoya and Train
我写的题解(可能比较冗长……)

@多维卷积@

对于含有多个变量的多项式,比如 \(f(x, y) = a_{00}x^0y^0 + a_{01}x^0y^1 + ...+ a_{0m}x^0y^m + a_{10}x^1y^1 + ... + a_{nm}x^ny^m\)
考虑怎么快速将它们相乘。

我们一样转为点值形式,通过代入单位根的二元组 \((w_n^i, w_m^j)\)(即令 \(x = w_n^i, y = w_m^j\) 得到的多项式的值)求出共 n * m 个点值,再对应位置相乘。
可以理解为将第一维相同的项放在一起,对第二维进行代入单位根;再将第二维相同的项放在一起,对第一位进行代入单位根。
逆变换同理。

也可以把 \(x^iy^j\) 看成 \(z^{i*(2*m+1)+j}\) 转为一维卷积来做。

一般运用在二维矩阵中的卷积问题,或是将二进制看成多维卷积 + 循环卷积来做。

@例题@ : @codechef - BUYLAND@ Buying Land
这里有一份题解
这种卷积题居然在 5, 6 年前就有人研究了……真可怕。

@循环卷积@

有一类特殊的卷积长这样:
\[c_{(i+j)\mod n} = \sum_{0\le i
我们将这类卷积称为循环卷积。

一般而言,循环卷积有两种解决方案。

一是使用正常的卷积,再把大于等于 n 的部分加到前面去。

二是将循环卷积转为点值表示,逐位相乘,再逆变换回去。
但是这个时候我们不能随便把卷积的长度变长变短,必须让它保持长度为 n。
注意到如果 \(x^n = 1\)\(x^{n+i} = x^i\),相当于指数取模。这个取模和循环卷积的取模非常的相似。
因此,可以通过代入 n 次单位根将循环卷积转换为点值表示。但是如果 n 不是 2 的幂就无法使用分治解决了。
因为无法使用分治,这个方法是 \(O(n^2)\)。但是在多次乘法的时候它具有一定的优越性,比如矩阵快速幂。

第二种方案,说明循环卷积与正常卷积具有一定的统一性,或者说正常卷积本就是循环卷积的一种特例。

@例题@ : @codechef - BIKE@ Chef and Bike,是 2017 冬令营的题。
这里有一份题解

@多项式求逆,除法与取模@

可以看我的这一篇博客

@多点求值与快速插值@

可以看我的这一篇博客

@多项式开方,对数,指数,三角与幂函数@

可以看我的这一篇博客

从入门到进阶再到出门已经安排得明明白白了。

转载于:https://www.cnblogs.com/Tiw-Air-OAO/p/10162034.html

你可能感兴趣的:(数据结构与算法,c/c++)