首先依次介绍FFT(快速傅里叶变换)前置知识:
复数及单位根
复变函数欧拉定理
单位根三大引理
多项式的系数与点值表示法
多项式的求值与插值(顺介绍拉格朗日插值法)
向量卷积
复数及单位根
我们知道复数可以写成
z
=
a
+
b
i
z=a+bi
z=a+bi,其中
a
a
a为实部,
b
i
bi
bi为虚部.而且一个复数可以在一个复平面上表示出来,而我们可以将其写成极坐标的形式
z
=
r
(
c
o
s
θ
+
i
s
i
n
θ
)
z=r(cos\theta+isin\theta)
z=r(cosθ+isinθ)而根据欧拉定理:
r
e
θ
i
=
=
c
o
s
θ
+
i
s
i
n
θ
re^{\theta i} == cos\theta + isin\theta
reθi==cosθ+isinθ
则有:
z
=
r
e
θ
i
z=re^{\theta i}
z=reθi,这里给出乘法的例子:
z
1
z
2
=
=
(
r
1
e
θ
1
i
)
(
r
2
e
θ
2
i
)
=
=
r
1
r
2
e
(
θ
1
+
θ
2
)
i
z_1z_2 == (r_1e^{\theta_1 i})(r_2e^{\theta_2 i})==r_1r_2e^{(\theta_1+\theta_2)i}
z1z2==(r1eθ1i)(r2eθ2i)==r1r2e(θ1+θ2)i
下面介绍单位根:
如果存在复数
ω
n
=
=
1
\omega^n==1
ωn==1,则其根被称作单位根,显然单位根的模为1,其中我们定义单位根:
ω
n
k
=
=
e
i
2
k
π
n
\omega_n^k==e^{i\frac{2k\pi}{n}}
ωnk==ein2kπ
其中定义主n次单位根(当k==1时):
ω
n
=
=
e
i
2
π
n
\omega_n==e^{i\frac{2\pi}{n}}
ωn==ein2π
而在程序中我们一般写成三角函数形式:
c
o
s
2
k
π
n
+
i
s
i
n
2
k
π
n
cos\frac{2k\pi}{n} + isin\frac{2k\pi}{n}
cosn2kπ+isinn2kπ
这里给出复数运算模板代码:
struct Complex
{
double r, i;
Complex() {}//无参函数不要写东西!易超时
Complex(double r, double i)
:r(r), i(i) {}
}f[N], g[N];
Complex operator + (const Complex & a, const Complex & b)
{return Complex(a.r + b.r, a.i + b.i);}
Complex operator - (const Complex & a, const Complex & b)
{return Complex(a.r - b.r, a.i - b.i);}
Complex operator * (const Complex & a, const Complex & b)
下面给出单位根的三大引理
ω
d
n
d
k
=
=
ω
n
k
(1)
\omega_{dn}^{dk}==\omega_n^k\tag1
ωdndk==ωnk(1)
ω
n
k
+
n
2
=
=
(
ω
n
k
)
2
=
=
ω
n
2
k
(2)
\omega_{n}^{k+\frac{n}{2}}==(\omega_{n}^{k})^2==\omega_{\frac{n}{2}}^k\tag2
ωnk+2n==(ωnk)2==ω2nk(2)
∑
i
=
0
n
−
1
(
w
n
k
)
i
=
=
0
(3)
\sum_{i=0}^{n-1}(w_n^k)^i==0\tag3
i=0∑n−1(wnk)i==0(3)
多项式的两种表示方法:
第一种非常常见的系数表示法,及
f
(
x
)
=
∑
i
=
0
n
a
i
x
i
f(x)=\sum_{i=0}^n a_ix^i
f(x)=∑i=0naixi,另外我们给出另一种表示方法,点值表示法,及用n+1种不同的取值代入
f
(
x
)
f(x)
f(x),组成的集合:
A
(
x
)
=
{
(
x
0
,
f
(
x
0
)
)
,
(
x
1
,
f
(
x
1
)
)
,
.
.
.
.
(
x
n
,
f
(
x
n
)
)
}
A(x)=\{(x_0,f(x_0)),(x_1,f(x_1)),....(x_n,f(x_n))\}
A(x)={(x0,f(x0)),(x1,f(x1)),....(xn,f(xn))}
我们分析两种表示方法:
系数表达式:
求值:我们可以使用秦九昭算法
O
(
n
)
O(n)
O(n)得出,
求和:两个表达式的各项系数相加,是
O
(
n
)
O(n)
O(n)的
求积:一个表达式相乘的各项都需要和另一个的各项相乘,是
O
(
n
2
)
O(n^2)
O(n2)的
点值表达式:
求值:我们使用拉格朗日插值法可以在
O
(
n
2
)
O(n^2)
O(n2)的出系数表达式并求值(插值的定义一会给出)
这里给出拉格朗日插值法的公式(n项表达式):
A
(
x
)
=
∑
k
=
0
n
−
1
y
k
∏
j
!
=
k
(
x
−
x
j
)
∏
j
!
=
k
(
x
k
−
x
j
)
A(x) = \sum_{k=0}^{n-1}y_k\frac{\prod_{j!=k}(x-x_j)}{\prod_{j!=k}(x_k-x_j)}
A(x)=k=0∑n−1yk∏j!=k(xk−xj)∏j!=k(x−xj)
并有模板:
P4781 【模板】拉格朗日插值
下面是模板代码:
const int N = 2e3+5;
const ll mod = 998244353;
ll a[N], b[N];
ll inv(ll a, ll b)
{
ll res = 1;
while(b)
{
if (b&1) res = res * a % mod;
b >>= 1;
a = a * a % mod;
}
return res % mod;
}
int main()
{
int n; ll k;
cin >> n >> k;
for (int i = 1; i <= n; i++)
scanf("%d%d", &a[i], &b[i]);
ll sum = 0;
for (int i = 1; i <= n; i++)
{
ll ans = b[i];
ll fa = 1, fb = 1;
for (int j = 1; j <= n; j++)
{
if (j == i) continue;
fa = (fa * (k - a[j] + mod)%mod)%mod;
fb = (fb * (a[i] - a[j] + mod)%mod)%mod;
}
ans = (ans * fa % mod * inv(fb, mod-2)%mod)%mod;
sum = (sum + ans)%mod;
}
printf("%lld\n", sum);
}
求和:两个表达式的各项点值的
y
y
y值相加,是
O
(
n
)
O(n)
O(n)的
求积:两个表达式的各项点值的
y
y
y值相乘,是
O
(
n
)
O(n)
O(n)的
我们不难发现,如果我们相求两个多项式的积
f
,
g
f,g
f,g(系数向量分别是
a
,
b
a,b
a,b),在点值表达式的情况下将十分高效.这时新表达式
h
h
h(系数向量是c)的第i项系数为:
c
i
=
∑
k
=
0
i
a
k
b
j
−
k
c_i=\sum_{k=0}^ia_kb_{j-k}
ci=k=0∑iakbj−k
我们则称
c
i
c_i
ci为
a
i
,
b
i
a_i,b_i
ai,bi的卷积,记作
a
⊗
b
a\otimes b
a⊗b,两个多项式相乘,就是对应的两个系数向量的卷积,
我们将一个点值表达式转化为系数表达式称为插值.
由此可见:如何提高多项式求值与插值的效率.决定着两个表达式相乘的效率!
话讲完,正式下面介绍FFT
首先看DFT(离散傅里叶变换)
我们将单位根带入到系数表达式(方便起见下式中
ω
\omega
ω用
w
w
w替代)中:
y
i
=
A
(
w
n
i
)
=
∑
j
=
0
n
−
1
w
n
i
j
a
j
y_i=A(w_n^i)=\sum_{j=0}^{n-1}w_n^{ij}a_j
yi=A(wni)=j=0∑n−1wnijaj
记作
y
=
D
F
T
(
a
)
y=DFT(a)
y=DFT(a)
每次求值为
O
(
n
)
O(n)
O(n)所以离散傅里叶变换最终时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
这样显然是不行的,毕竟直接相乘的时间复杂度都是
O
(
n
2
)
O(n^2)
O(n2)
然后就有了FFT:
我们对于一个n项系数表达式(n为2的幂数):
A
(
x
)
→
a
=
[
a
0
,
a
1
.
.
.
.
.
a
n
−
1
]
A(x)\rightarrow a =[a_0,a_1.....a_{n-1}]
A(x)→a=[a0,a1.....an−1]
可以分成:
A
[
0
]
(
x
)
→
a
[
0
]
=
[
a
0
,
a
2
,
a
4
.
.
.
.
.
.
a
n
−
2
]
A^{[0]}(x)\rightarrow a^{[0]}=[a_0,a_2,a_4......a_{n-2}]
A[0](x)→a[0]=[a0,a2,a4......an−2]
A
[
1
]
(
x
)
→
a
[
1
]
=
[
a
1
,
a
3
,
a
5
.
.
.
.
.
.
a
n
−
1
]
A^{[1]}(x)\rightarrow a^{[1]}=[a_1,a_3,a_5......a_{n-1}]
A[1](x)→a[1]=[a1,a3,a5......an−1]
这时我们发现一个及其有趣的事实:
A
(
x
)
=
A
[
0
]
(
x
2
)
+
x
A
[
1
]
(
x
2
)
A(x)=A^{[0]}(x^2)+xA^{[1]}(x^2)
A(x)=A[0](x2)+xA[1](x2)
而根据(1),(2)两个引理:我们最终有:
{
A
(
w
n
k
)
=
A
[
0
]
(
w
n
2
k
)
+
w
n
k
A
[
1
]
(
w
n
2
k
)
A
(
w
n
k
+
n
2
)
=
A
[
0
]
(
w
n
2
k
)
−
w
n
k
A
[
1
]
(
w
n
2
k
)
\left\{ \begin{aligned} A(w_n^k)&=A^{[0]}(w_{\frac{n}{2}}^k)+w_n^kA^{[1]}(w_{\frac{n}{2}}^k)\\ A(w_n^{k+\frac{n}{2}})&=A^{[0]}(w_{\frac{n}{2}}^k)-w_n^kA^{[1]}(w_{\frac{n}{2}}^k) \end{aligned} \right.
⎩⎨⎧A(wnk)A(wnk+2n)=A[0](w2nk)+wnkA[1](w2nk)=A[0](w2nk)−wnkA[1](w2nk)
于是问题得以递归解决:
T
(
n
)
=
O
(
n
2
)
+
2
T
(
n
2
)
T(n) = O(\frac{n}{2}) + 2T(\frac{n}{2})
T(n)=O(2n)+2T(2n)
时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
逆FFT
我们的多项式不可以FFT过去弄不过来了!比较简单,
因为:
y
k
=
∑
j
=
0
n
−
1
a
j
w
n
k
j
y_k=\sum_{j=0}^{n-1}a_jw_n^{kj}
yk=j=0∑n−1ajwnkj
所以:
a
j
=
1
n
∑
j
=
0
n
−
1
y
k
w
n
−
k
j
a_j=\frac{1}{n}\sum_{j=0}^{n-1}y_kw_n^{-kj}
aj=n1j=0∑n−1ykwn−kj
证明略,总之就是证明原向量矩阵可逆
所以我们稍加改动就可以逆FFT,就是将单位根的指数取相反数,最后结果再乘1/n
所以两个多项式
f
,
g
f,g
f,g的乘积,使其对应系数向量
a
,
b
a,b
a,b的卷积!可以有:
a
⊗
b
=
D
F
T
2
n
−
1
(
D
F
T
2
n
(
a
)
D
F
T
2
n
(
b
)
)
a\otimes b =DFT^{-1}_{2n}(DFT_{2n}(a) DFT_{2n}(b))
a⊗b=DFT2n−1(DFT2n(a)DFT2n(b))
不过我们很少书写递归代码,这里使用迭代写法;
这里还牵扯到两个点:位逆序置换 蝴蝶操作,不说了…直接上代码吧
P3803 【模板】多项式乘法(FFT)
const int N = 3e6+5;
const double pi = acos(-1);
struct Complex
{
double r, i;
Complex() {}
Complex(double r, double i)
:r(r), i(i) {}
}f[N], g[N];
Complex operator + (const Complex & a, const Complex & b)
{return Complex(a.r + b.r, a.i + b.i);}
Complex operator - (const Complex & a, const Complex & b)
{return Complex(a.r - b.r, a.i - b.i);}
Complex operator * (const Complex & a, const Complex & b)
{return Complex(a.r * b.r - a.i * b.i, a.i * b.r + a.r * b.i);}
int rev[N];
int len, lim = 1;
void init(int n)
{
while(lim <= n) lim <<= 1, len++;
for (int i = 0; i < lim; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len-1));
}
void FFT(Complex *a, int op)//op为1是FFT运算,-1是FFT逆运算
{
for (int i = 0; i < lim; i++)
if (i < rev[i]) swap(a[i], a[rev[i]]);
for (int dep = 1; dep <= log2(lim); dep++)
{
int m = 1 << dep;
Complex wn = Complex(cos(2.0 * pi / m), op * sin(2.0 * pi / m));
for (int k = 0; k < lim; k += m)
{
Complex w = Complex(1, 0);
for (int j = 0; j < m / 2; j++)
{
Complex t = w * a[k + j + m / 2];
Complex u = a[k + j];
a[k + j] = u + t;
a[k + j + m / 2] = u - t;
w = w * wn;
}
}
}
if (op == -1) for (int i = 0; i < lim; i++)
a[i].r /= lim;
}
int main()
{
int n, m;
cin >> n >> m;
init(n + m);
for (int i = 0; i <= n; i++) scanf("%lf", &f[i].r);
for (int i = 0; i <= m; i++) scanf("%lf", &g[i].r);
FFT(g, 1); FFT(f, 1);
for (int i = 0; i <= lim; i++) f[i] = f[i] * g[i];
FFT(f, -1);
for (int i = 0; i <= n + m; i++)
printf("%d ", (int)(f[i].r + 0.5));
return 0;
}
FFT分治
【模板】分治 FFT
根据cdq分治思想,
我们在计算多项式的一个系数的时候,要统计之前的所有影响。我们区间折半递归解决,先解决左区间的数值,然后统计其对右区间的影响,累加到右区间。
洛谷模板需要取模,故模板代码使用NTT,值得注意的是,洛谷模板的模数998244353,符合NTT所要求的素数类型,且存在原根为3。
下面是模板代码:
const int N = 1e6+5;
const int G = 3;
const ll mod = 998244353;
ll f[N], g[N];
ll a[N], b[N];
int rev[N], n;
int len, lim = 1;
void init(int n)
{
lim = 1; len = 0;
while(lim <= n) lim <<= 1, len++;
for (int i = 0; i < lim; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len-1));
}
ll _pow(ll a, ll b) {
//....
}
inline void calcrev(int logn)
{
rev[0]=0;
for(int i = 1; i < (1 << logn); i++)
rev[i]=(rev[i>>1]>>1)|((i&1)<<(logn-1));
}
void NTT(ll *a, int led, int op)//op为1是FFT运算,-1是FFT逆运算
{
int lim = 1 << led;
//......
}
void cdq_fft(int l, int r, int logn)
{
if (logn <= 0) return ;
if (l >= n) return ;
int mid = (l + r) >> 1;
ll t = _pow(r - l, mod-2);
cdq_fft(l, mid, logn - 1);
calcrev(logn);
memset(a + (r - l) / 2, 0, sizeof(ll) * (r - l) / 2);
memcpy(a, f+l, sizeof(ll) * (r - l) / 2);
memcpy(b, g, sizeof(ll) * (r - l) );
NTT(a, logn, 1); NTT(b, logn, 1);
for (int i = 0; i < r - l; i++) a[i] = a[i] * b[i] % mod;
NTT(a, logn, -1);
for (int i = (r - l) / 2; i < r - l; i++)
f[l + i] = (f[l+i] + a[i]) % mod;
cdq_fft(mid, r, logn-1);
}
int main()
{
cin >> n;
init(n);
for (int i = 1; i < n; i++) scanf("%lld", &g[i]);
f[0] = 1;
cdq_fft(0, lim, len);
for (int i = 0; i < n; i++)
printf("%lld ", (f[i] + mod) % mod);
puts("");
return 0;
}
本文深入解析快速傅里叶变换(FFT)原理及其在多项式乘法中的应用,涵盖复数运算、单位根概念、DFT与FFT算法,以及逆FFT实现。通过对比系数与点值表示法,阐述了FFT在提升多项式运算效率上的关键作用。

1778

被折叠的 条评论
为什么被折叠?



