【数学】FFT和FWT

FFT和FWT原理和代码

曾经写过一篇牛头不对马嘴的傅里叶级数的课程小论文,摘取一点放出来(FFT),FWT只有代码

FFT

原理

傅里叶级数

三角形式

​ $$f(x)=\frac{a_0}{2}+\sum_{k=1}^{\infty}{(a_k\cos kx+b_k\sin kx)}$$ ,其中

a0=1πππf(x)dx,an=1πππf(x)cosnxdx,(n=0,1,2),bn=1πππf(x)sinnxdx,(n=0,1,2).\begin{aligned} a_0&=\frac{1}{\pi}\int_{-\pi}^{\pi}f(x) dx, \\a_n&=\frac{1}{\pi}\int_{-\pi}^{\pi}f(x)\cos nx dx, (n=0,1,2\dots), \\b_n&=\frac{1}{\pi}\int_{-\pi}^{\pi}f(x)\sin nx dx, (n=0,1,2\dots) . \end{aligned}

指数形式

f(t)=c0+n=1(cneiωnt+cneiωnt)=n=+cneiωntf(t)=c_{0}+\sum_{n=1}^{\infty}\left(c_{n} e^{i \omega_{n} t}+c_{-n} e^{-i \omega_{n} t}\right)=\sum_{n=-\infty}^{+\infty} c_{n} e^{i \omega_{n} t}

傅里叶变换

傅里叶积分定理

对于 $$T$$ 为周期的周期函数 $$f(t)$$ ,考虑 $$T\rightarrow+\infty$$ ,则在 $$[-\frac{T}{2}, \frac{T}{2}]$$ 上的周期函数 $$f(t)$$ 可以表示任意符合傅里叶级数展开条件的函数,此时 $$\omega=\frac{2\pi}{T}\rightarrow 0$$ ,则 $$\omega_n=n\omega=(n+1)\omega-n\omega$$ 是连续变化的,即 $$\omega_n=n\Delta \omega$$ ,则

cn=Δω2πT/2T/2f(t)eiωntdtc_{n}=\frac{\Delta \omega}{2 \pi} \int_{-T / 2}^{T / 2} f(t) e^{-i \omega_{n} t} dt

代回原式可得,

f(t)=12π+(T/2T/2f(t)eiωntdt)eiωntΔω=12π+(+f(t)eiωntdt)eiωntdωf(t)=\frac{1}{2 \pi} \sum_{-\infty}^{+\infty}\left(\int_{-T / 2}^{T / 2} f(t) e^{-i \omega_n t} d t\right) e^{i \omega_n t} \Delta \omega=\frac{1}{2 \pi} \int_{-\infty}^{+\infty}\left(\int_{-\infty}^{+\infty} f(t) e^{-i \omega_n t} d t\right) e^{i \omega_n t} d \omega

傅里叶变换

​ 记 $$F(\omega)=\int_{-\infty}^{+\infty} f(t) e^{-i \omega t} d t$$ ,任何一个函数 $$f(t)$$ 都可以通过变换得到 $$F(\omega)$$ ,

ψ[f(t)]=F(ω)=+f(t)eiωtdt\psi[f(t)]=F(\omega) =\int_{-\infty}^{+\infty} f(t) e^{-i \omega t} d t

这个对函数的变换称为傅里叶变换,称 $$F(\omega)$$ 是 $$f(t)$$ 的象函数,称 $$f(t)$$ 是 $$F(\omega)$$ 的象原函数,称 $$|F(\omega)|$$ 为 $$f(t)$$ 的振幅谱,$$\arg{F(\omega)} $$ 为函数 $$f(t)$$ 的相位谱。

由以上变换易得傅里叶逆变换

ψ1[F(ω)]=f(t)=12π+F(ω)eiωtdω\psi^{-1}[F(\omega)]=f(t)=\frac{1}{2 \pi} \int_{-\infty}^{+\infty} F(\omega) e^{i \omega t} d \omega

多项式

系数表达法

我们一般将 $$N-1$$ 次多项式记为 $$A(x) =\sum_{n=0}^{N-1}a_n x^n$$ ,它由 $$n$$ 个系数 $$a_0, a_1, \dots, a_n$$ 唯一确定,称系数表达法。

点值表达法

我们还常用另一种记法,令 $$y_i=A(x_i), i=0,1,\dots, N-1$$ ,相当于在多项式的时域上取 $$N$$ 个点: $$(x_0, y_0), (x_1,y_1), \dots, (x_{N-1}, y_{N-1})$$ ,这些点也可以唯一确定该多项式,称点值表达法

傅里叶逆变换(IDFT)

对于 $$N-1$$ 次多项式 $$A(x) =\sum_{n=0}^{N-1}a_n x^n$$ ,取 $$x_k=e^{\frac{2 \pi i k}{N}}=\omega_N^k(k=0,1, \ldots N-1)$$ ,其中 $$i$$ 是虚数单位,设 $$y_{k}=A(x_k)=\sum_{n=0}^{N-1}a_n\omega_N^{kn}$$ ,由前文傅里叶逆变换类比

an=1Nk=0N1(ωNn)kyka_{n}=\frac{1}{N} \sum_{k=0}^{N-1} (\omega_N^{-n})^ky_{k}

逆变换相当于多项式 $$B(x)=\sum_{k=0}^{N-1}\frac{y_k}{N}x^k$$ 在 $${\omega_N^{-j}}, j=0,1,\cdots,N-1$$ 的点值表示。

单位根

代数中,若 $$w^n=1$$ ,则称 $$w$$ 为 $$n$$ 次单位根。

在复平面上,以原点为圆心,$$1$$ 为半径作单位圆,将单位圆 $$n$$ 等分,从原点以 $$n$$ 等分点为终点作向量,称辐角为正且最小的向量表示的复数 $$\omega_n=e^{\frac{2 \pi i}{n}} $$ 为主 $$n$$ 次单位根,由复数的运算性质得 $$\omega_n^n=1$$ 。

从辐角为正且最小的开始编号,结合复数的运算性质得

ωnk=e2πikn=cos(2πkn)+isin(2πkn),k=0,1,n1,\omega_n^k=e^{\frac{2 \pi i k}{n}}=\cos \left(\frac{2 \pi k}{n}\right)+i \cdot \sin \left(\frac{2 \pi k}{n}\right), \quad k=0,1, \ldots n-1,

引理1:

ωdndk=(e2πidn)dk=e2πikn=wnk\omega_{dn}^{d k}=\left(e^{\frac{2\pi i}{d n}}\right)^{d k}=e^{\frac{2 \pi i k}{n}}=w_{n}^{k}

引理2:

ωnn2=(e2πin)n2=eπi=1\omega_{n}^{\frac{n}{2}}=\left(e^{\frac{2\pi i}{n}}\right)^{\frac{n}{2}}=e^{\pi i}=-1

引理3:$$n$$ 为偶数,$$n$$ 个 $$n$$ 次单位复数根组成的集合即为 $$\frac{n}{2}$$ 个 $$\frac{n}{2}$$ 次单位复数根组成的集合。

证明:即证$$0\leq k\leq n-1, 0\leq j\leq \frac{n}{2}-1$$ 且 $$k, j\in N$$ , $${(\omega_n^{k})^2}={\omega_{\frac{n}{2}}^{j}}$$.

由引理1和引理2易得 $$\omega_n^{j+\frac{n}{2}}=\omega_n^j\omega_n^{\frac{n}{2}}=-\omega_n^j$$ , 因此 $${(\omega_n^{k})^2}={(\omega_{n}^{j})^2}={\omega_{n}^{2j}}={\omega_{\frac{n}{2}}^{j}}$$ ,证毕。

实现

快速求解多项式

分治算法

令 $$A_{0}(x)=a_{0}+a_{2} x+a_{4} x^{2}+\cdots a_{n-2} x^{n / 2-1},A_{1}(x)=a_{1}+a_{3} x+a_{5} x^{2}+\cdots a_{n-1} x^{n / 2-1}$$,

则 $$ A(x)=a_{0}+a_{1} x+a_{2} x^{2}+a_{3} x^{3} \cdots a_{n-1} x^{n-1} =A_{0}\left(x^{2}\right)+x A_{1}\left(x^{2}\right)$$

求 $$ A(x)$$ 在 $${\omega_N^k}, ~k=0, 1, \cdots, N-1$$ 处的值相当于求 $$A_0$$ 和 $$A_1$$ 在 $${(\omega_N^k)^2}, ~k=0, 1, \cdots, N-1$$ 的值,由引理3得,上述操作相当于求 $$A_0$$ 和 $$A_1$$ 在 $${\omega_{\frac{N}{2}}^{j}}, 0\leq j\leq \frac{N}{2}-1$$ 的值,于是将问题规模缩小呈原来的一半,分别对二者在 $$\frac{N}{2}$$ 上进行FFT,直到只剩一个常数项

算法复杂度为 $$O(n \log n)$$

特别说明,对于 $$N$$ 非 $$2$$ 的整次幂的FFT,只需补充为 $$0$$ 的项即可,不影响结果。

另外,通过逆变换的式子我们能看出,它也可以使用FFT分治算法计算,只不过点值取了倒数。

朴素的快速傅里叶变换分治算法的实现代码如下:

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
typedef complex<double> comp; //comp即复数类型
const int MAXN = 1000005;
const comp I(0, 1);
const double PI = acos(-1); //定义常量 i 和 pi
comp A[MAXN * 3], B[MAXN * 3], tmp[MAXN * 3], ans[MAXN * 3];
void fft(comp F[], int N, int sgn = 1) //F是系数数组,N是数组长度
{ //由于IDFT与DFT在运算形式上几乎一样,我们使用一个函数来完成运算,用sgn=1表示DFT,sgn=-1表示IDFT
if (N == 1) return; //递归边界
memcpy(tmp, F, sizeof(comp) * N); //数组拷贝一份
for (int i = 0; i < N; i++)
if(i&1) //奇数项
*(F + i / 2 + N / 2) = tmp[i]; //将奇数项放到数组后半部分
else
*(F+i/2) = tmp[i]; //将偶数项放到数组前半部分
fft(F, N / 2, sgn), fft(F + N / 2, N / 2, sgn); //分治FFT处理偶数项和奇数项
comp Wn=exp(2 * PI / N * sgn * I), w=1;
//Wn为单位根,w表示幂
int n=N>>1;
for(int i=0;i<n;i++,w*=Wn)//这里的w相当于公式中的k
{
tmp[i]=F[i]+w*F[i+n],
tmp[i+n]=F[i]-w*F[i+n]; //引理3
}
memcpy(F, tmp, sizeof(comp) * N);
}

蝴蝶变换

分治算法已经有了理想的复杂度,但是算法实现时有反复的数组复制操作,我们尝试优化常数,考虑能否提前确定序列中每个元素最终的位置。以 $$N=8$$ 为例:

观察易得,最终序列的元素下标是原序列元素下标二进制的翻转,又称蝴蝶变换(暂未找到严格证明),由此我们可以提前确定序列中每个元素的位置,迭代进行快速傅里叶变换,不需要递归写法了,优化了常数。

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
typedef complex<double> comp; //comp即复数类型
const int MAXN = 1000005;
const comp I(0, 1);
const double PI = acos(-1); //定义常量 i 和 pi
comp A[MAXN * 3], B[MAXN * 3], tmp[MAXN * 3], ans[MAXN * 3];
int rev[MAXN * 3];
void fft(comp F[], int N, int sgn = 1)
{
int bit = __lg(N); //二进制的最高位
for (int i = 0; i < N; ++i) //蝴蝶变换
{ //rev[i]记录的是a_i最终序列的位置
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
//记i的二进制为head+tail,+代表拼接而不是加法
//其中tail是最后一位即i&1,head是前面那些位即i>>1
//需要得到tail+rev(head部分),tail部分即为tail<<(bit-1)
//rev[i>>1]为rev(head部分)+0(最后一位),因此rev(head部分)为rev[i>>1]>>1
//从而二者作或运算即最终结果
if (i < rev[i]) //避免反复交换
swap(F[i], F[rev[i]]);
}
for (int l = 1; l < N; l <<= 1) //枚举合并前的区间长度
{
comp Wn = exp(sgn * PI / l * I); //单位根
for (int i = 0; i < N; i += (l<<1) ) //遍历起始点
{
comp w(1,0); //幂
for (int k = i; k < i + l; ++k, w *= Wn)
{
comp g = F[k], h = F[k + l] * w;
F[k] = g + h, F[k + l] = g - h;
}
}
}
}

应用:多项式乘法(卷积)

对于$$A(x) =\sum_{n=0}^{N-1}a_n x^n, B(x) =\sum_{n=0}^{N-1}b_n x^n$$ ,有

C(x)=A(x)B(x)=i=02n1Cixi, 其中 Ci=(k=0iakbik)C(x)=A(x)B(x)=\sum_{i=0}^{2 n-1} C_{i} x^{i}, \text { 其中 } C_{i}=\left(\sum_{k=0}^{i} a_{k} b_{i-k}\right)

计算的时间复杂度是 $$O(n^2)$$ ,这种形式系数下,没有优化的方法,考虑改变系数的形式进行优化。在多项式的时域上取 $$N$$ 个点: $$(x_0, A(x_0)), (x_1, A(x_1)), \dots, (x_{N-1}, A(x_{N-1}))$$ 和 $$(x_0, B(x_0)), (x_1, B(x_1)), \dots, (x_{N-1}, B(x_{N-1}))$$ ,可以通过 $$O(n)$$ 的计算得到 $$(x_0, C(x_0)), (x_1, C(x_1)), \dots, (x_{N-1}, C(x_{N-1}))$$ ,因此这 $$N$$ 个点唯一确定多项式 $$C(x)$$ 的系数,由前面知识可得通过傅里叶逆变换(IDFT)可以确定该系数。

实现了快速傅里叶变换,多项式乘法 $$C(x)=A(x)B(x)$$ ,其中C(x)C(x) 的次数是 $$N-1$$ ,计算可以取 $$N$$ 个点: $${\omega_N^k}, ~k=0, 1, \cdots, N-1$$ ,计算出 $$C(x)$$ 在上述点的值,通过傅里叶逆变换确定 $$C(x)$$ 的系数。由上文我们知道快速傅里叶变换的复杂度是 $$O(n \log n)$$ ,其余读入、乘积、输出操作复杂度均为 $$O(n)$$ ,总复杂度通过快速傅里叶变换优化到 $$O(n \log n)$$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
int n = read(), m = read(), N = 1 << __lg(n + m + 1) + 1;
//读入n,m,将N-1补成2的整次幂
for (int i = 0; i <= n; ++i)
A[i] = read();
for (int i = 0; i <= m; ++i)
B[i] = read();
//读入两个多项式的系数
fft(A, N), fft(B, N);
//分别计算点值
for (int i = 0; i < N; ++i)
ans[i] = A[i] * B[i]; //计算乘积多项式的点值
fft(ans, N, -1); //傅里叶逆变换
for (int i = 0; i <= n + m; ++i)
printf("%d ", int(ans[i].real() / N + 0.1)); // +0.1规避浮点误差
return 0;
}

FWT

FWT(C)=FWT(A)×FWT(B)FWT(C)=FWT(A)\times FWT(B) ,对于或、与、异或运算

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
void FWT_or(int *a, int opt)
{
for (int i = 1; i < N; i <<= 1)
for (int p = i << 1, j = 0; j < N; j += p)
for (int k = 0; k < i; ++k)
if (opt == 1)
a[i + j + k] = (a[j + k] + a[i + j + k]) % MOD;
else
a[i + j + k] = (a[i + j + k] + MOD - a[j + k]) % MOD;
}
void FWT_and(int *a, int opt)
{
for (int i = 1; i < N; i <<= 1)
for (int p = i << 1, j = 0; j < N; j += p)
for (int k = 0; k < i; ++k)
if (opt == 1)
a[j + k] = (a[j + k] + a[i + j + k]) % MOD;
else
a[j + k] = (a[j + k] + MOD - a[i + j + k]) % MOD;
}
void FWT_xor(int *a, int opt)
{
for (int i = 1; i < N; i <<= 1)
for (int p = i << 1, j = 0; j < N; j += p)
for (int k = 0; k < i; ++k)
{
int X = a[j + k], Y = a[i + j + k];
a[j + k] = (X + Y) % MOD;
a[i + j + k] = (X + MOD - Y) % MOD;
if (opt == -1) //如果不是在模意义下的话,开一个long long,然后把逆元变成直接除二就好了。
a[j + k] = 1ll * a[j + k] * inv2 % MOD, a[i + j + k] = 1ll * a[i + j + k] * inv2 % MOD;
}
}

其他快速变换(待补)

快速数论变换(NTT)

快速傅里叶变换利用了单位复根的特殊性质,大大减少了运算,但是每个复数系数的实部和虚部是一个正弦及余弦函数,因此大部分系数都是浮点数,计算量会比较大,且浮点数的计算可能会导致误差。在离散正交变换的理论中,已经证明在复数域内,具有循环卷积特性的唯一变换是DFT,所以在复数域中不存在具有循环卷积性质的更简单的离散正交变换,因此提出了以数论为基础的具有循环卷积性质的快速数论变换(NTT)。

MTT

由于NTT仅能针对特定模数,通过数论的相关知识推导,得到找到三个符合要求的模数,在这三个模数意义下分别FFT然后利用中国剩余定理合并一下,即三模数NTT,用发明的人的首字母记作MTT算法。

快速沃尔什变换(FWT)/ 快速莫比乌斯变换(FMT)

快速傅里叶变换可以用来优化卷积,而对于 $$C_{k}=\sum_{i+j=k} A_{i} * B_{j}$$ ,如果换成 $$i|j, i&j, \cdots$$ 等逻辑运算,就不能使用快速傅里叶变换了,但是受其思想启发,对于每种逻辑运算构造不同的运算方法,再还原到结果的系数,且运算中所有系数均为整数,运算速度相对较快。