多项式乘法 —— FFT(快速傅里叶变换)
参考
多项式
a(x)=a0+a1x+a2x2+...+an−1xn−1=∑i=0n−1aixib(x)=b0+b1x+b2x2+...+bn−1xn−1=∑i=0n−1bixi
多项式存在一个性质:如果最高项为 xn−1 ,那么至少 n 个不同的点就可以确定一个多项式。
比如:一次函数可以用两个点唯一确定,二次函数可以用三个点唯一确定。
多项式乘法
多项式相乘有两种方法:系数乘法和点值乘法。
系数相乘:c(x)=a(x)b(x)=∑k=02n−2ckxk,其中 ck=∑i+j=kaibj
除此之外,根据“点确定多项式”这个方式,可以找到另一种乘法:点值相乘。
乘积最高项为 2n-2,用至少 2n-1 个点可以确定,因此在两个多项式分别找到 2n - 1 个点。
取 [x0,x1,x2,...,x2n−2] 分别代入两个多项式:
a(x0)a(x1)a(x2)...a(x2n−2)=111...1x0x1x2x2n−2x02x12x22x2n−22............x02n−2x12n−2x22n−2x2n−22n−2 a0a1a2...a2n−2
b(x0)b(x1)b(x2)...b(x2n−2)=111...1x0x1x2x2n−2x02x12x22x2n−22............x02n−2x12n−2x22n−2x2n−22n−2 b0b1b2...b2n−2
得到点值 [a(x0),a(x1),a(x2),...,a(x2n−2)] 和 [b(x0),b(x1),b(x2),...,b(x2n−2)]。
相乘得到 c(x) 的点值 [a(x0)b(x0),a(x1)b(x1),a(x2)b(x2),...,a(x2n−2)b(x2n−2)]。
然后求解线性方程组计算 c(x) 系数 [c0,c1,c2,...,c2n−2]
a(x0)b(x0)=c0+c1x0+c2x02+...+c2n−2x02n−2a(x1)b(x1)=c0+c1x1+c2x12+...+c2n−2x12n−2a(x2)b(x2)=c0+c1x2+c2x22+...+c2n−2x22n−2...a(x2n−2)b(x2n−2)=c0+c1x2n−2+c2x2n−22+...+c2n−2x2n−22n−2
矩阵表示:
a(x0)b(x0)a(x1)b(x1)a(x2)b(x2)...a(x2n−2)b(x2n−2)=111...1x0x1x2x2n−2x02x12x22x2n−22............x02n−2x12n−2x22n−2x2n−22n−2 c0c1c2...c2n−2
c0c1c2...c2n−2=111...1x0x1x2x2n−2x02x12x22x2n−22............x02n−2x12n−2x22n−2x2n−22n−2 −1a(x0)b(x0)a(x1)b(x1)a(x2)b(x2)...a(x2n−2)b(x2n−2)
例子:
a(x)=1+2x+x2b(x)=1−2x+x2
此时它们的乘积的最高次数为 4,我们分别找 5 个点:(−2,1),(−1,0),(0,1),(1,4),(2,9) 和 (−2,9),(−1,4),(0,1),(1,0),(2,1)。
直接相乘纵坐标得到 (−2,9),(−1,0),(0,1),(1,0),(2,9)。
通过这 5 个点确定乘积为 1−2x2+x4。
对于系数相乘,复杂度是 O(n2);对于点值相乘,复杂度是 O(n)。
但是在点值相乘中,两个多项式求点值、乘积多项式求系数的复杂度仍是 O(n2)。FFT 的作用就是将这两个过程的时间复杂度降到 O(nlogn)。
下面来逐步推导如何得到最终的 FFT 算法。
首先注意到,对于多项式代入求值,如果它是奇函数或偶函数,当我们选择互为相反数的点时,求值的计算量减小了一半。
比如,对于偶函数 x2,需要计算八个点,我们选择 [−4,−3,−2,−1,1,2,3,4],只需要代入大于 0 的四个点进行求值即可。奇函数同理。
对于不具备奇偶性的多项式,我们可以把它分解为奇函数与偶函数之和(任何函数可以分解成奇函数和偶函数之和)。
P(x)=p0+p1x+p2x2+...+pn−1xn−1,需要计算 n 个点,选择点 [±x1,±x2,...,±xn/2]。
P(x)=Pe(x2)+xPo(x2),奇函数是 xPo(x2),偶函数是 Pe(x2)。
Pe 的系数是 [p0,p2,p4,...],Po 的系数是 [p1,p3,p5,...],需要代入计算的点是 [x12,x22,...,xn/22]。
然后 Pe 和 Po 当作新的多项式,递归地重复上面的步骤,最终需要代入计算的点可以缩减到一个,这个点选择为 1。
这里有需要注意的地方:
- 每一层递归需要计算的点会减半,因此最开始的点的数量应该是 2 的幂。
- 只有点是相反数时,代入计算量才可以减半。为了让每一层迭代的点都是一对一对的相反数,最开始的 [±x1,±x2,...,±xn/2] 需要引入复数。
比如多项式 P(x)=x3+x2−x−1,最高次数为 3,选择 4 个点。
迭代过程 [x1,−x1,x2,−x2]、[x12,x22]、[x14],最后一个点选为 1,即 x14=1。可以解得最开始应该选择 [1,−1,i,−i]。
比如多项式 P(x)=x5−x3+x2−x−1,最高次数为 5,至少选择 6 个点,因此选择 8 个点。
迭代过程为 [x1,−x1,x2,−x2,x3,−x3,x4,−x4]、[1,−1,i,−i]、[1,−1]、[1]。
P(x)=p0+p1x+p2x2+...+pd−1xd−1,需要计算 n 个点,n≥(d+1),n=2k,k∈Z。
这 n 个点的选择是固定的,即 zn=1 的复数根。
根据欧拉公式 eiθ=cosθ+isinθ,取 θ=2kπ,ei2kπ=1。
复数 z 写成极坐标形式:z=r(cosθ+isinθ)=reiθ,r 为模长,θ 为角度。
求解 zn=1 等价于求解 rneinθ=ei2kπ。
极坐标模长部分 rn=1,得到 r=1;角度部分 nθ=2kπ,得到 θ=n2kπ。
得到 zn=1 的解是 zk=cosn2kπ+isinn2kπ=en2πki。由于只需要 n 个解,取 k=0,1,2,...,n−1 即可。
这 n 个点将复平面单位圆平分成 n 份。
记 ω=en2πi,选择在 [ω0,ω1,ω2,...,ωn−1] 这 n 个点上求值。
P(ωj)=Pe(ω2j)+ωjPo(ω2j)P(−ωj)=Pe(ω2j)−ωjPo(ω2j)j∈{0,1,2,...(n/2−1)}
由于 −ωj=ωj+n/2,最终化简为:
P(ωj)=Pe(ω2j)+ωjPo(ω2j)P(ωj+n/2)=Pe(ω2j)−ωjPo(ω2j)j∈{0,1,2,...(n/2−1)}
其中 Pe 的系数为 [p0,p2,...,pn−2],Po 的系数为 [p1,p3,...,pn−1]。
求值过程用矩阵表示:
P(ω0)P(ω1)P(ω2)...P(ωn−1)=111...1ω0ω1ω2ωn−1ω0ω2ω4ω2(n−1)............ω0ωn−1ω2(n−1)ω(n−1)(n−1)p0p1p2...pn−1
FFT 就是选择 [ω0,ω1,ω2,...,ωn−1] 这 n 个点,然后用分治的思想,递归处理代入求值。
constexpr double PI = 3.141592653589;
using cd = std::complex<double>;
void fft(std::vector<cd>& p) {
int n = p.size(); // n为2的幂
if (n == 1) {
return;
}
std::vector<cd> pe(n/2), po(n/2);
for (int i = 0; 2 * i < n; i++) {
// 根据奇偶性拆分
pe[i] = p[2 * i];
po[i] = p[2 * i + 1];
}
// 递归求值
fft(pe);
fft(po);
const double angle = 2 * PI / n;
const cd w(std::cos(angle), std::sin(angle)); // omega的一次方
cd wk(1);
for (int i = 0; 2 * i < n; i++) {
p[i] = pe[i] + wk * po[i];
p[i + n / 2] = pe[i] - wk * po[i];
wk *= w;
}
}
目前为止,讨论清楚了 FFT 如何选点、代入求值,接下来讨论如何根据值求系数。
c(x)=a(x)b(x),选择 n 个点,n 为 2 的幂。
a(ω0)b(ω0)a(ω1)b(ω1)a(ω2)b(ω2)...a(ωn−1)b(ωn−1)=111...1ω0ω1ω2ωn−1ω0ω2ω4ω2(n−1)............ω0ωn−1ω2(n−1)ω(n−1)(n−1)c0c1c2...cn−1
c0c1c2...cn−1=111...1ω0ω1ω2ωn−1ω0ω2ω4ω2(n−1)............ω0ωn−1ω2(n−1)ω(n−1)(n−1)−1a(ω0)b(ω0)a(ω1)b(ω1)a(ω2)b(ω2)...a(ωn−1)b(ωn−1)=n1111...1ω0ω−1ω−2ω−(n−1)ω0ω−2ω−4ω−2(n−1)............ω0ω−(n−1)ω−2(n−1)ω−(n−1)(n−1)a(ω0)b(ω0)a(ω1)b(ω1)a(ω2)b(ω2)...a(ωn−1)b(ωn−1)
通过矩阵可以看到,求系数的过程和求值的过程非常近似,只有参数发生改变。
constexpr double PI = 3.141592653589;
using cd = std::complex<double>;
void ifft(std::vector<cd>& p) {
int n = p.size(); // n为2的幂
if (n == 1) {
return;
}
std::vector<cd> pe(n/2), po(n/2);
for (int i = 0; 2 * i < n; i++) {
// 根据奇偶性拆分
pe[i] = p[2 * i];
po[i] = p[2 * i + 1];
}
// 递归
ifft(pe);
ifft(po);
const double angle = -2 * PI / n;
const cd w(std::cos(angle), std::sin(angle));
cd wk(1);
for (int i = 0; 2 * i < n; i++) {
p[i] = pe[i] + wk * po[i];
p[i + n / 2] = pe[i] - wk * po[i];
// 每轮除以2,等效于整个除以n
p[i] /= 2;
p[i + n / 2] /= 2;
wk *= w;
}
}