快速傅立叶变换

# 前置知识

# 多项式系数表示法

A(x)=a0+a1x+a2x2++anxnA(x)=a_0+a_1x+a_2x^2+\dots +a_nx^n

系数表示法的好处
可以直接利用参数 xx 计算多项式值,即直接代入上式 O(n)O(n) 地计算

# 多项式点值表示法

首先要知道一个定理:任意 n+1n+1 个不同点均可以确定一个 nn 次多项式

x1xn+1:{a0+a1x1+a2x12++anx1n=y1a0+a1x2+a2x22++anx2n=y2a0+a1xn+a2xn2++anxnn=yna0+a1xn+1+a2xn+12++anxn+1n=yn+1x_1\rightarrow x_{n+1}: \left\{\begin{aligned} a_0&+a_1x_1+a_2x_1^2+\dots+a_nx_1^n=y_1\\ a_0&+a_1x_2+a_2x_2^2+\dots+a_nx_2^n=y_2\\ &\dots\\ a_0&+a_1x_n+a_2x_n^2+\dots+a_nx_n^n=y_n\\ a_0&+a_1x_{n+1}+a_2x_{n+1}^2+\dots+a_nx_{n+1}^n=y_{n+1} \end{aligned}\right.

(1x1x12x1n1x2x22x2n1xn+1xn+12xn+1n)=1i,jn+1(xixj)0\left(\begin{aligned} &1&\quad&x_1&\quad&x_1^2&\quad&\dots&\quad&x_1^n&\\ &1&\quad&x_2&\quad&x_2^2&\quad&\dots&\quad&x_2^n&\\ &\dots&\\ &1&\quad&x_{n+1}&\quad&x_{n+1}^2&\quad&\dots&\quad&x_{n+1}^n& \end{aligned}\right) =\prod\limits_{1\le i,j\le n+1}(x_i-x_j)\neq 0

即使用了 n+1n+1 个特殊点,这个方程组用高斯消元便可以解

点表示法的好处
A(x)B(x)A(x)B(x)[0,n+m][0,n+m] 次,所以要取 n+m+1n+m+1 个点
A:(x1,A(x1)),(x2,A(x2)),,(xn+m+1,A(xn+m+1))A:(x_1,A(x_1)),(x_2,A(x_2)),\dots,(x_{n+m+1},A(x_{n+m+1}))
B:(x1,B(x1)),(x2,B(x2)),,(xn+m+1,B(xn+m+1))B:(x_1,B(x_1)),(x_2,B(x_2)),\dots,(x_{n+m+1},B(x_{n+m+1}))
若求 C(xi),C(xi)=A(xi)B(xi)C(x_i),\;C(x_i)=A(x_i)B(x_i)
C:(x1,A(x1)B(x1)),(x2,A(x2)B(x2)),,(xn+m+1,A(xn+m+1)B(xn+m+1))\therefore C:(x_1,A(x_1)B(x_1)),(x_2,A(x_2)B(x_2)),\dots,(x_{n+m+1},A(x_{n+m+1})B(x_{n+m+1}))
时间复杂度 O(nm)O(n+m)O(nm)\rightarrow O(n+m)

# 卷积

也就是多项式乘法
对于系数分别为 [a][a] 的多项式 AA 与系数分别为 [b][b] 的多项式 BB ,其卷积出来的多项式 CC 的第 kk 项系数 ckc_k
ck=i+j=kaibj=i=0kaibkic_k=\sum\limits_{i+j=k}a_ib_j=\sum\limits_{i=0}^ka_ib_{k-i}

# 算法引导

我们得到两个系数表示法的多项式,想要卷积出来一个系数表示法的多项式,在乘法时可以借助点表示法去乘从而优化时间,但这样时间复杂度就卡在了系数表示法和点表示法互相转换上面,所以有一个前提
系数表示法 quick\stackrel{quick}{\rightleftharpoons} 点值表示法

# 快速求点值

# 复数与单位根

引入特殊点:复平面上的单位根

复数:
a+bia+bi

运算:

  • 相加:(a+bi)+(c+di)=(a+c)+(b+d)i(a+bi)+(c+di)=(a+c)+(b+d)i ,平行四边形对角线
  • 相乘:(a+bi)(c+di)=(acbd)+(ad+bc)i(a+bi)(c+di)=(ac-bd)+(ad+bc)i ,向量长度为两向量长的乘积,复角为两向量复角的和

单位根:

将单位圆从 xxnn 等分,获得 nn 次单位根
eiθ=cosθ+isinθe^{i\theta}=cos\theta+isin\theta
ωnk=cos(2πkn)+isin(2πkn)=e2πkni\omega_n^k=cos(2\pi\frac kn)+i\sin(2\pi\frac kn)=e^{2\pi\frac kn i}

# 单位根性质

1.ωnk=ω2n2k1.\omega_n^k=\omega_{2n}^{2k}

分成 nn 份第 kk 个和分成 2n2n 份第 2k2k 个是相同的

2.ωnk+n2=ωnk2.\omega_n^{k+\frac n2}=-\omega_n^k

相当于跨越了半个圆

3.ωnkωnl=ωnk+l3.\omega_n^k\omega_n^l=\omega_n^{k+l}

可以代入欧拉公式证明
ωnk=e2πkni\omega_n^k=e^{2\pi\frac kni}
ωnl=e2πlni\omega_n^l=e^{2\pi\frac lni}
ωnkωnl=e2πin(k+l)=ωnk+l\omega_n^k\omega_n^l=e^{\frac 2\pi in(k+l)}=\omega_n^{k+l}

其实之所以引入单位根也是因为这个特性,相乘等于系数相加,和 xixj=xi+jx^ix^j=x^{i+j} 的特性一样

# DFT

# 思想

A(x)A(x) 系数:(a0,a1,,an1)(a_0,a_1,\dots,a_{n-1})
代入一组单位根: (ωn0,ωn1,,ωnn1)(\omega_n^0,\omega_n^1,\dots,\omega_n^{n-1}) 快速求出 (A(ωn0),A(ωn1),,A(ωnn1))(A(\omega_n^0),A(\omega_n^1),\dots,A(\omega_n^{n-1}))
A(x)A(x) 中看
我们可以用分治的方式将其划分为两个相等的部分,对于每一部分求完直接合并

A(x)=(a0+a2x2++an2xn2)+(a1x1+a3x3++an1xn1)=(a0+a2x2++an2xn2)+x(a1+a3x2++an1xn2)\begin{aligned} A(x)&=(a_0+a_2x^2+\dots+a_{n-2}x^{n-2})+(a_1x^1+a_3x^3+\dots+a_{n-1}x^{n-1})\\ &=(a_0+a_2x^2+\dots+a_{n-2}x^{n-2})+x(a_1+a_3x^2+\dots+a_{n-1}x^{n-2}) \end{aligned}

这里即奇偶划分
然后用 xxx2x^2 换元
A1(x)=a0+a2x+a4x2++an2xn21A_1(x)=a_0+a_2x+a_4x^2+\dots+a_{n-2}x^{\frac n2-1}
A2(x)=a1+a3x+a5x2++an1xn21A_2(x)=a_1+a_3x+a_5x^2+\dots+a_{n-1}x^{\frac n2-1}
A(x)=A1(x2)+A2(x2)A(x)=A_1(x^2)+A_2(x^2)

# 方法

分类讨论一下

k[0,n2)k\in[0,\frac n2)
A(ωnk)=A1((ωnk)2)+ωnkA2((xnk)2)=A1(ωn2k)+ωnkA2(ωn2k)\begin{aligned}A(\omega_n^k)&=A_1((\omega_n^k)^2)+\omega_n^kA_2((x_n^k)^2)\\&=A_1(\omega_{\frac n2}^k)+\omega_n^kA_2(\omega_{\frac n2}^k)\end{aligned}

k[n2,n1]k\in[\frac n2,n-1]
A(ωnk+n2)=A1((ωnk+n2)2)+ωnk+n2((ωnk)2)=A1(ωn2k+n)ωnkA2(ωn2k+n)=A1(ωn2kωnn)ωnkA2(ωn2kωnn)=A1(ωn2k)ωnkA2(ωn2k)\begin{aligned} A(\omega_n^{k+\frac n2})=&A_1((\omega_n^{k+\frac n2})^2)+\omega_n^{k+\frac n2}((\omega_n^k)^2)\\ =&A_1(\omega_n^{2k+n})-\omega_n^kA_2(\omega_n^{2k+n})\\ =&A_1(\omega_n^{2k}\omega_n^n)-\omega_n^kA_2(\omega_n^{2k}\omega_n^n)\\ =&A_1(\omega_{\frac n2}^k)-\omega_n^kA_2(\omega_{\frac n2}^k) \end{aligned}

这样分类进行递归,时间复杂度为 O(nlogn)O(nlogn)

# IDFT

(ωnk,A(ωnk))A(x)=c0+c1x++cn1xn1(\omega_n^k,A(\omega_n^k))\quad A(x)=c_0+c_1x+\dots+c_{n-1}x^{n-1}
yk=A(ωnk)y_k=A(\omega_n^k) ,则 ck=i=0n1yi(ωnk)ic_k=\sum\limits_{i=0}^{n-1}y_i(\omega_n^{-k})^i
原系数 ak=ckna_k=\frac{c_k}n
想快速求出上面的 c[0,n1]c_{[0,n-1]} ,可以把式子当做一个关于 yy 的多项式
B(x)=y0+y1x++yn1xn1B(x)=y_0+y_1x+\dots+y_{n-1}x^{n-1}
ck=B(ωnk)c_k=B(\omega_n^{-k})B(ωnk)B(\omega_n^{-k}) 的一个个求解还需 FFTFFT 从而推出 ckc_kA(x)A(x)
那么本质就是一遍 FFTFFT 推出点表示,再来一遍推出系数表示
第一遍求 A(ωn0)A(xnn1)A(\omega_n^0)\rightarrow A(x_n^{n-1})
第二遍求 B(ωn0)B(ωn(n1))B(\omega_n^0)\rightarrow B(\omega_n^{-(n-1)})

ck=i=0n1yk(ωnk)i=i=0n1(j=0n1aj(ωni)j)(ωnk)i=i=0n1(j=0n1aj(ωnjk)i)=j=0n1aj(i=0n1(ωnjk)i)\begin{aligned} c_k &=\sum_{i=0}^{n-1} y_k\left(\omega_n^{-k}\right)^i \\ &=\sum_{i=0}^{n-1}\left(\sum\limits_{j=0}^{n-1} a_j\left(\omega_n^i\right)^j\right)\left(\omega_n^{-k}\right)^i \\ &=\sum_{i=0}^{n-1}\left(\sum\limits_{j=0}^{n-1} a_j\left(\omega_n^{j-k}\right)^i\right) \\ &=\sum_{j=0}^{n-1} a_j\left(\sum_{i=0}^{n-1}\left(\omega_{n}^{j-k}\right)^i\right) \end{aligned}

S(x)=1+x+x2++xnS(x)=1+x+x^2+\dots+x^n

S(ωnk):k{0S(ωnk)=1+ωnk+ωn2k++ωn(n1)kωnk(S(ωnk))=ωnk+ωn2k++ωn0(1ωnk)S=0k0:ωnk1S=0=0S(ωnk)=S(1)=nS(\omega_n^k):k\left\{\begin{aligned} \neq 0&\quad &S(\omega_n^k)=1+\omega_n^k+\omega_n^{2k}+\dots+\omega_n^{(n-1)k}\\ &&\omega_n^k(S(\omega_n^k))=\omega_n^k+\omega_n^{2k}+\dots+\omega_n^0\\ &&(1-\omega_n^k)S=0\\ &&\because k\neq 0:\omega_n^k\neq 1\quad \therefore S=0\\ \\ =0 &&S(\omega_n^k)=S(1)=n \end{aligned}\right.

=j=0n1(S(ωnjk))=\sum\limits_{j=0}^{n-1}(S(\omega_n^{j-k}))

因为只有在 j=kj=k 时候不得 00 会被记入后面是 nn

=nak\therefore =na_k

# 蝴蝶变换

由于 FFTFFT 本来就涉及很多实数运算,常数非常大,如果此时我们再使用递归的话常数会更大,考虑使用递推来实现
写一组系数与递归后的系数进行对比:

a0000a1001a2010a3011a4100a5101a6110a7111a0a2a4a6a1a3a5a7a0000a4100a2010a6110a1001a5101a3011a7111\begin{aligned}\underline{\overset{000}{a_{0}} \quad \overset{001}{a_{1}} \quad \overset{010}{a_{2}} \quad\overset{011}{a_3}}\quad&\underline{\overset{100}{a_4}\quad\overset{101}{a_5}\quad\overset{110}{a_6}\quad\overset{111}{a_7}} \\ \underline{a_{0} \quad a_{2}}\; \quad \underline{a_{4}\quad a_6}\;\quad &\underline{a_1\quad a_3}\quad \;\underline{a_5\quad a_7} \quad\\ \underset{000}{\underline{a_{0}}} \quad \underset{100}{\underline{a_{4}}} \quad \underset{010}{\underline{a_{2}}}\quad\underset{110}{\underline{a_6}}\quad&\underset{001}{\underline{a_1}}\quad\underset{101}{\underline{a_5}}\quad\underset{011}{\underline{a_3}}\quad\underset{111}{\underline{a_7}}\end{aligned}

发现最后一次操作后,下标与原下标互为翻转: 00012100020001_2\rightarrow1000_2

那么我们可以先得出最后的序列,然后向上进行操作模拟递归的回溯操作
revirev_i 代表将 ii 二进制翻转后的数
bitbitn+mn+m 最多需要的二进制位数
rev[i] = (rev[i >> 1] >> 1) | (1 << (bit - 1))

# 程序

已有最高次为 nn 的多项式 AA 的系数 [a][a]
已有最高次为 mm 的多项式 BB 的系数 [b][b]
AB=CA*B=C 的所有系数

const int N = 3000010;
const double PI = acos(-1.0);

int n, m;
struct Complex { // 复数结构体
        double x, y;
        Complex friend operator + (Complex a, Complex b) { return {a.x + b.x, a.y + b.y}; }
        Complex friend operator - (Complex a, Complex b) { return {a.x - b.x, a.y - b.y}; }
        Complex friend operator * (Complex a, Complex b) { return {a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x}; }
} a[N], b[N]; // 两个多项式的点表示
int rev[N];   // 分治时候的二进制表示对应的结果二进制表示,即反过来了
int bit, tot; // 二进制上的位数,总个数

inline void FFT (Complex a[], int inv) {
        for (int i = 0; i < tot; i ++) if (i < rev[i]) swap(a[i], a[rev[i]]); // 变成正确的分治结果位置(只能换一半,防止换回来
        for (int mid = 1; mid < tot; mid <<= 1) { // 枚举分块的块长度
                Complex w1 = {cos(PI / mid), inv * sin(PI / mid)}; // 这也是把整个单位圆平均切成mid个后出现的 \omega^1
                for (int i = 0; i < tot; i += mid * 2) { // 两个两个块向后跳,枚举每一段
                        Complex wk = {1, 0}; // w(n, 0)单位一开始
                        for (int j = 0; j < mid; j ++, wk = wk * w1) { // 把区间里面数枚举一遍,且wk要往上跑一格
                                Complex x = a[i + j], y = wk * a[i + j + mid]; // x把左边提出,y把右边提出
                                a[i + j] = x + y, a[i + j + mid] = x - y;      // 左边和右边重构
                        }
                }
        }
}

int main() {
        cin >> n >> m;
        for (int i = 0; i <= n; i ++) cin >> a[i].x; // 把输入的系数塞入实部
        for (int i = 0; i <= m; i ++) cin >> b[i].x; // 把输入的系数塞入虚部
        while ((1 << bit) < n + m + 1) bit ++; // 次数最多到n+m+1,所以利用n+m+1记录二进制位数
        tot = 1 << bit; // 在二进制位数下计算刚好达不到的那个位数的数
        for (int i = 0; i < tot; i ++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1)); // 每个rev[i]初始化为i的二进制逆转
        FFT(a, 1); FFT(b, 1); // 完成a和b的点表示
        for (int i = 0; i < tot; i ++) a[i] = a[i] * b[i]; // 点表示法内完成两方程合并
        FFT(a, -1); // 逆向做一遍得到系数表示
        for (int i = 0; i <= n + m; i ++) cout << (int)(a[i].x / tot + 0.5) << " "; // 防止精度丢失,要向上0.5再强转扔精度
        return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Last Updated: 10/14/2023, 7:51:49 PM