多项式卷积与数论变换(NTT)

做多项式题就像嗑药,出多项式题就像贩毒。


多项式表示

讲到多项式有系数表示法和点表示法两种情况,其关系是在于:

如果有一个 次多项式 ,其点表示法为 ,有:


多项式卷积

有三个不定次多项式 ,如果有 ,则有:

这就是多项式卷积,通俗来讲就是多项式乘法。暴力计算时间复杂度为 ,通过 可以优化到

关于傅里叶变换

在求多项式卷积的时候,傅里叶变换经历了三个过程:

  1. 快速傅里叶变换():用一组特殊点(复数单位根)快速计算出点表示法。
  2. 离散傅里叶变换():通过奇偶性分类让一组单位根快速计算其 值。
  3. 离散傅里叶逆变换():通过点表示法逆转换为系数表示法。

时间分别是

而通过用迭代的方法来代替 的递归,这个优化称为蝴蝶变换

为什么可以使用复数单位根?

在上一章我们已经知道了复数单位根的定义,所以这里不多赘述。

根据欧拉公式,可以得到

所以,有 ,因此,我们能够在很快的时间内使用 ,以免不必要的麻烦。

这是 优化多项式卷积的过程,我们在上一章详细讲过。


原根

由欧拉定理可知:。因此,存在最小正整数 使得 ,这个时候, 被称为 的阶,记作

关于阶有许多性质:

  • 两两不同余。
  • ,则有
  • ,则有


  • 则有:,即两者互为充要条件。


  • 则有:

最后两个性质在阶的四则运算中发挥了重要作用。


原根的定义

,若有 ,且 ,则 是模 的原根。

用人话来讲,如果存在一个数 使得 在模 意义下两两不同,则 的原根。

原根判定定理

我们这里只考虑 的情况(毕竟 手玩都可以解决)。设 ,则 是模 的原根的充要条件是:对于 的任意质因子 都满足

其证明需要用到裴蜀定理和欧拉定理,可利用反证法解决。


原根个数定理

对于一个数 ,如果其存在原根,则其原根个数为

证明

有原根 ,则:

时,则有 ,即 也是 的原根。

而满足 个,所以原根亦如此。

证毕


原根存在定理

一个数 存在原根当且仅当 ,其中 为奇质数,

其证明需要 个步骤,较为麻烦,这里不进行讲述,可见 的详细证明。


最小原根数量级定理

有原根,则其最小原根 满足

这个定理由王元在 年提出,为暴力计算原根提供了依据。


数论变换

考虑下面一个问题:

题目简介

对于一个 次多项式 和一个 次多项式 ,有 ,给定一个 ,求 取模的结果。

数据范围: 为质数。

显然,对于在 double 下使用复数计算的 并不能很好地计算取模意义下的多项式卷积问题,那么,就要清楚本章的主角——数论变换()了。

中,我们用原根代替复数单位元进行运算,以避免精度出错和无法取模的问题。

为什么原根可以代替复数单位元

的计算中,我们用到了复数单位元的几条性质:

  • 对于 ,当 时除外,保证了点值的合法性。
  • ,用于分治。
  • ,用于分治。
  • 时,,用于逆变换。

而同样的,原根也具有相同的性质,我们设

  • 在模 意义下互不相同。

两者结合,可以得到:

然后我们用这些玩意儿代替原来的复数即可。代码结构与 极其相似。

一般情况下, 的常数会比 小很多,所以很多多项式题都是用 来做的。

一般取质数 有:

或者

参考代码
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// ----- Eternally question-----
// Problem: P3803 【模板】多项式乘法(NTT)
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3803
// Memory Limit: 500 MB
// Time Limit: 2000 ms
// Written by: Eternity
// Time: 2023-01-10 21:55:17
// ----- Endless solution-------

#include<bits/stdc++.h>
#define re register
typedef long long ll;
template<class T>
inline void read(T &x)
{
x=0;
char ch=getchar(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1){ read(x),read(x1...); }
template<class T>
inline void write(T x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
template<>
inline void write(bool x){ putchar(x?'1':'0'); }
template<>
inline void write(char c){ putchar(c); }
template<>
inline void write(char *s){ while(*s!='\0') putchar(*s++); }
template<>
inline void write(const char *s){ while(*s!='\0') putchar(*s++); }
template<class T,class ...T1>
inline void write(T x,T1 ...x1){ write(x),write(x1...); }
template<class T>
inline bool checkMax(T &x,T y){ return x<y?x=y,1:0; }
template<class T>
inline bool checkMin(T &x,T y){ return x>y?x=y,1:0; }
const int MAXN=4e6+10;
const int P=998244353,G=3,InvG=332748118;
int N,M;
inline ll qPow(ll a,ll b)
{
ll res=1;
while(b)
{
if(b&1) res=res*a%P;
a=a*a%P;b>>=1;
}
return res;
}
ll A[MAXN],B[MAXN];
int Tot,Bit,Rev[MAXN];
inline void NTT(ll a[],int inv)
{
for(int i=0;i<Tot;++i)
if(i<Rev[i]) std::swap(a[i],a[Rev[i]]);
for(int mid=1;mid<Tot;mid<<=1)
{
auto w1=qPow(inv==1?G:InvG,(P-1)/(mid<<1));
for(int i=0;i<Tot;i+=mid*2)
{
auto wk=1;
for(int j=0;j<mid;++j,wk=wk*w1%P)
{
auto x=a[i+j],y=a[i+j+mid]*wk%P;
a[i+j]=(x+y)%P,a[i+j+mid]=(x-y+P)%P;
}
}
}
if(inv==-1)
{
ll iv=qPow(Tot,P-2);
for(int i=0;i<Tot;++i) a[i]=a[i]*iv%P;
}
}
int main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
read(N,M);
for(int i=0;i<=N;++i) read(A[i]);
for(int i=0;i<=M;++i) read(B[i]);
while((1<<Bit)<=N+M) ++Bit;
Tot=1<<Bit;
for(int i=0;i<Tot;++i) Rev[i]=(Rev[i>>1]>>1)|((i&1)<<(Bit-1));
NTT(A,1),NTT(B,1);
for(int i=0;i<Tot;++i) A[i]=A[i]*B[i]%P;
NTT(A,-1);
for(int i=0;i<=N+M;++i) write(A[i]%P,' ');
return 0;
}
/*

*/

优化高精度乘法也和上面的代码实现相同,不过多了一个进位操作而已。其构造也与 类似。


接下来,你可以解锁下列章节: