更多文章可以在本人的个人小站:https://kaiserwilheim.github.io 查看。
转载请注明出处。
引入
今天AJ给大家留了一个作业:
多项式相乘。
f ( x ) = ( x 2 + 3 x − 1 ) g ( x ) = ( 2 x 2 + x − 5 ) f ( x ) × g ( x ) = ? \begin{gathered} f(x) = (x^2 + 3x - 1) \\ g(x) = (2x^2 + x - 5) \\ f(x) \times g(x) = ? \end{gathered} f(x)=(x2+3x−1)g(x)=(2x2+x−5)f(x)×g(x)=?
大家都很认真、很用心地做了出来。
f ( x ) × g ( x ) = ( x 2 + 3 x − 1 ) ( 2 x 2 + x − 5 ) = x 2 × g ( x ) + 3 x × g ( x ) − g ( x ) = ( 2 x 4 + x 3 − 5 x 2 ) + ( 6 x 3 + 3 x 2 − 15 x ) + ( 2 x 2 + x − 5 ) = 2 x 4 + x 3 − 5 x 2 + 6 x 3 + 3 x 2 − 15 x + 2 x 2 + x − 5 = 2 x 4 + 7 x 3 − 4 x 2 − 16 x + 5 \begin{aligned} f(x) \times g(x) & = (x^2 + 3x - 1) (2x^2 + x - 5) \\ & = x^2 \times g(x) + 3x \times g(x) - g(x) \\ & = (2x^4 + x^3 - 5x^2) + (6x^3 + 3x^2 - 15x) + (2x^2 + x - 5) \\ & = 2x^4 + x^3 - 5x^2 + 6x^3 + 3x^2 - 15x + 2x^2 + x - 5 \\ & = 2x^4 + 7x^3 - 4x^2 - 16x + 5 \end{aligned} f(x)×g(x)=(x2+3x−1)(2x2+x−5)=x2×g(x)+3x×g(x)−g(x)=(2x4+x3−5x2)+(6x3+3x2−15x)+(2x2+x−5)=2x4+x3−5x2+6x3+3x2−15x+2x2+x−5=2x4+7x3−4x2−16x+5
看起来很复杂,对吗?
但是,精通数学的王哥在纸上写写画画几十秒钟之后,得出来的答案跟正确答案也是一样的。
惊呆了的tüe问王哥他用的是什么办法。
王哥回答:“ 傅里叶变换 。”
多项式乘法的本质
王哥说:“我们知道,所有多项式都拥有如下的形式:
f ( x ) = a 0 x n + a 1 x n − 1 + a 2 x n − 2 + ⋯ + a n − 2 x 2 + a n − 1 x + a n f(x) = a_0 x^n + a_1 x^{n-1} + a_2 x^{n-2} + \cdots + a_{n-2} x^2 + a_{n-1} x +a_n f(x)=a0xn+a1xn−1+a2xn−2+⋯+an−2x2+an−1x+an
我们也可以把一个多项式写成这样的形式:
f ( x ) = ∑ i = 0 n a i x n − i f(x) = \sum_{i=0}^n a_i x^{n-i} f(x)=i=0∑naixn−i
而两个多项式相乘 f ( x ) × g ( x ) = h ( x ) f(x) \times g(x) = h(x) f(x)×g(x)=h(x) 的时候,相乘的结果的第 k k k 项系数 h k − 1 h_{k-1} hk−1 等于所有 f i f_{i} fi 与 g k − i g_{k-i} gk−i 乘积之和。
所以,
h k = ∑ i = 0 k − 1 f i × g k − 1 − i h_k = \sum_{i=0}^{k-1} f_i \times g_{k-1-i} hk=i=0∑k−1fi×gk−1−i
这样的方法算出的结果是与两个多项式的次数相关的。”
或者(对于OIer)通俗一点来说,假设两个多项式的次数分别为 n n n 和 m m m,那么这个算法的时间复杂度是 O ( n m ) O(nm) O(nm) 的。
给个模板题的44分代码:
#include<bits/stdc++.h>
const int N = 100010;
#define ll long long
using namespace std;
inline int read()
{
register int X = 0;
register char ch = 0;
while((ch < 48) || (ch > 57))ch = getchar();
while((ch >= 48) && (ch <= 57))X = X * 10 + (ch ^ 48), ch = getchar();
return X;
}
int n, m;
ll f[N], g[N], s[N];
void mul(ll *s, ll *f, ll *g)
{
f_or(int k = 0; k < n + m - 1; k++)
f_or(int i = 0; i <= k; i++)
s[k] += f[i] * g[k - i];
}
int main()
{
scanf("%d%d", &n, &m); n++; m++;
f_or(int i = 0; i < n; i++)f[i] = read();
f_or(int i = 0; i < m; i++)g[i] = read();
mul(s, f, g);
f_or(int i = 0; i < n + m - 1; i++)printf("%lld ", s[i]);
return 0;
}
DFT
多项式的点值表达
为了简便表达,我们使用 f k f_k fk 来代表 多项式 f ( x ) f(x) f(x) 的第 k + 1 k+1 k+1 项系数。
今天AJ的作业是昨天的延伸:
给定一个 n n n 次多项式的 n + 1 n+1 n+1 个点值,要求我们求出这个多项式。
(还让武嘉给了样例,而武嘉给的样例是1,3,5,7,9,114514)
全班只有王哥和某盒子做了出来。广告:CoolHezi
他们用的是什么方法呢?
拉格朗日插值。
想要了解拉格朗日插值,可以参考我的这个博客:拉格朗日插值
现在我们只需要知道,给定了 n + 1 n+1 n+1 个任意点值,可以求出来经过这几个点的一个 n n n 次多项式。
如果两个多项式 f ( x ) f(x) f(x) 和 g ( x ) g(x) g(x) 在相同纵坐标上取点(设其分别为 f ( x ) f(x) f(x) 与 g ( x ) g(x) g(x)) 后相乘所得的结果( f ( x ) × g ( x ) f(x) \times g(x) f(x)×g(x))等于两函数相乘后对应纵坐标处的点值( h ( x ) h(x) h(x))。
举个栗子:
f ( x ) = x 2 + 3 x − 1 g ( x ) = 2 x 2 + x − 5 \begin{aligned} f(x) &= x^2 + 3x - 1 \\ g(x) &= 2x^2 + x -5 \end{aligned} f(x)g(x)=x2+3x−1=2x2+x−5
我们分别取 x ∈ [ − 1 , 1 ] x \in [-1,1] x∈[−1,1] 内的整数点所对应的点值:
我们可以清楚的看到:
f ( − 1 ) = − 3 f ( 0 ) = − 1 f ( 1 ) = 3 g ( − 1 ) = − 4 g ( 0 ) = − 5 g ( 1 ) = − 2 \begin{aligned} f(-1) & = -3 & f(0) & = -1 & f(1) & = 3 \\ g(-1) & = -4 & g(0) & = -5 & g(1) & = -2 \end{aligned} f(−1)g(−1)=−3=−4f(0)g(0)=−1=−5f(1)g(1)=3=−2
相乘之后可得:
h ( − 1 ) = 12 h ( 0 ) = 5 h ( 1 ) = − 6 \begin{aligned} h(-1) & = 12 & h(0) & = 5 & h(1) & = -6 \end{aligned} h(−1)=12h(0)=5h(1)=−6
检验一下:
但想要求出 h ( x ) h(x) h(x) ,我们至少需要4+1=5个点值。
怎么办?
多找几个啊。
于是我们就可以求出最终的多项式。
这就是FT的算法流程。
“把系数表达转换为点值表达”的算法叫做DFT
“把点值表达转换为系数表达”的算法叫做IDFT(DFT的逆运算)
P.S:
- 从一个多项式的系数表达确定其点值表达的过程称为求值(毕竟求点值表达的过程就是取了 n 个 x 然后扔进了多项式求了 n 个值出来);
- 而求值运算的逆运算(也就是从一个多项式的点值表达确定其系数表达)被称为插值。
F(Fourier)和T(Transform)有了,那F(Fast)呢?
单位根与复数
但是最终我们并没有觉得有什么可以加以利用的良好性质啊。
我们这些蒟蒻只会利用一些有理数和一些简单的无理数来进行一些简单的计算,再难一点的就不会了。
(而且这个多项式的次数和系数稍微一大就bz了)
准确来说,我们最终还是需要 n + m n+m n+m 个点的求值与相乘,最终得到的时间复杂度与 O ( m n ) O(mn) O(mn) 实际上差不太多,而且很可能在某些情况下更劣一些。
但是,法国数学家 傅里叶 横空出世,找了一些毒瘤数据代入,结果发现可以分治而使时间复杂度降低。
而他代入的正是单位根 ω n + 1 0 → n ω_{n+1}^{0 \to n} ωn+10→n 。
复数
首先我们需要介绍复数。
已经会了的可以跳过。
(p.s.:下面我们使用的所有 \sqrt{\quad} 标识都指的是平方根,算术平方根已使用±来标记。)
复数的概念
虚数
我们所学的数轴是一条直线。
每一个有理数都能完美地与数轴上的某一个点一一对应。
+ 2 +\sqrt2 +2 、 + 3 +\sqrt3 +3 等无理数也能很好地对应在数轴上。
但是人们说:“那 + − 1 +\sqrt{-1} +−1 怎么办啊?”
我们找不到与 + − 1 +\sqrt{-1} +−1 相对应的点。
而 + − 1 +\sqrt{-1} +−1 又的的确确存在。
怎么办?
于是人们发明了一个概念:虚数。
而 + − 1 +\sqrt{-1} +−1 在虚数里面叫做虚数单位,用 i i i 表示。
所以, i 2 = − 1 i^2=-1 i2=−1 。
但是又有人会问:“那 − − 1 -\sqrt{-1} −−1 又怎么办?”
用 − i -i −i 呗。
但是在数轴上,人们仍然找不到对应虚数的点。数轴上的每一个点都对应了一个实数,没有办法找到任何一个新的点来对应虚数了。
所以,人们就在数轴的 0 处添加了一条新的数轴,来代表虚数。这条新的数轴垂直于代表实数的数轴,单位是 i i i 。
复数及其运算
复数形似 a + b i a+bi a+bi 。
其中 a a a 称为实部( ℜ \Re ℜ ), b b b 称为虚部( $\Im $ )。
复数也有加减乘除等运算。
复数加减时实部虚部分别加减:
( a + b i ) ± ( c + d i ) = ( a ± c ) + ( b ± d ) i (a+bi) \pm (c+di) = (a \pm c) + (b \pm d)i (a+bi)±(c+di)=(a±