[关闭]
@dxbdly 2023-07-16T11:58:04.000000Z 字数 2584 阅读 238

快速数论变换(NTT)

OI-算法


部分内容参考文献
本文作者,引用或转载请注明原文链接

1. 前言

上篇文章,我们介绍了求解多项式乘法的一种算法 ,不清楚的读者请阅读文章

需要用到复数类,常数较大且容易出现精度误差。

可以很好的解决这两个问题,它是一种基于 原根 进行 的算法。

2. 原根

仍采取点值表示,研究数值表示转点值表示,点值表示转数值表示两个问题。

解决这两个问题的方法,与 完全一样,只不过 改变了点值表示中取的点。

它采取 原根 代替单位复根,以此避免复数计算

,且 ,则称满足 的最小的 在模 意义下的阶,记为

原根的定义

对于正整数 ,整数 ,若满足 ,则称 为模 意义下的原根

注意:

原根个数不唯一,且如果模数 有原根,则必有 (不重要,不证了)

后文 均指模 意义下的原根

原根的性质

前面提到了,我们希望用原根代替单位根,那么,先要构造一个不重复的 项序列。

重要性质:

为质数,那么 各不相同
证明:考虑费马小定理:

所以只要取 即可满足,但我们在 中还用到了关于单位复根的几个性质。

可以发现原根均可满足:

所以:

在模 意义下有:

3. 实现

由于 要在模意义下进行,所以我们得先选好模数。

我们一般选择模数为 ,它的原根是 ,逆元是

对于 ,将所有 换为 即可,记得每一步操作都需要取模。

对于 ,还是构造逆矩阵,将 的底数改为逆元,可同理与 一起进行,注意输出时要乘 的逆元

4. 模板

模板题

代码:

  1. //The code is from dxbdly
  2. #include<bits/stdc++.h>
  3. #define int long long
  4. using namespace std;
  5. inline int read() {
  6. int x = 0;
  7. char c = getchar();
  8. bool f = 0;
  9. while(!isdigit(c)) f |= (c == '-'), c = getchar();
  10. while(isdigit(c)) x = (x * 10) + (c ^ 48), c = getchar();
  11. return f ? -x : x;
  12. }
  13. const int maxn = 3e6 +5, mod = 998244353, g = 3, gi = 332748118;
  14. int n, m;
  15. int g1[maxn], g2[maxn], rev[maxn];
  16. inline int Ksm(int a, int b) {
  17. int res = 1;
  18. while(b) {
  19. if(b & 1) res = res * a % mod;
  20. a = a * a % mod, b >>= 1;
  21. }
  22. return res;
  23. }
  24. inline void Change(int f[], int len) {
  25. for(register int i = 0; i < len; ++i) {
  26. rev[i] = (rev[i >> 1] >> 1);
  27. if(i & 1) rev[i] |= (len >> 1);
  28. }
  29. for(register int i = 0; i < len; ++i)
  30. if(i < rev[i]) swap(f[i], f[rev[i]]);
  31. }
  32. inline void NTT(int f[], int len, int op) {
  33. Change(f, len);
  34. for(register int h = 2; h <= len; h <<= 1) {
  35. int wn = Ksm((op == 1 ? g : gi), (mod - 1) / h);
  36. for(register int i = 0; i < len; i += h) {
  37. int w = 1;
  38. for(register int k = i; k < i + h / 2; ++k) {
  39. int G = f[k], H = (w * f[k + h / 2]) % mod;
  40. f[k] = (G + H) % mod , f[k + h / 2] = (G - H + mod) % mod;
  41. w = (w * wn) % mod;
  42. }
  43. }
  44. }
  45. }
  46. signed main() {
  47. n = read() + 1, m = read() + 1;
  48. for(register int i = 0; i < n; ++i) g1[i] = (read() + mod) % mod;
  49. for(register int i = 0; i < m; ++i) g2[i] = (read() + mod) % mod;
  50. int len = 1;
  51. while(len < n * 2 || len < m * 2) len <<= 1;
  52. NTT(g1, len, 1), NTT(g2, len, 1);
  53. for(register int i = 0; i < len; ++i) g1[i] = (g1[i] * g2[i]) % mod;
  54. NTT(g1, len, -1);
  55. int inv = Ksm(len, mod - 2);
  56. for(register int i = 0; i < m + n - 1; ++i) printf("%d ", (g1[i] * inv) % mod);
  57. return 0;
  58. }

注意:
全部都需取模,中间运算可能爆 ,要开

5.

是一种从 ,发展而来的算法,需要读者对 有比较清晰的了解。

的缺点与优点很明显:

优点: 常数更小,且不需要进行复数运算,精度误差小。

缺点: 十分依赖模数,如果题目对模数有限制,则只能考虑 (任意模数 )

作为多项式乘法中可以说最常用的算法,需要大家仔细了解。

希望此文章能给您带来帮助,感谢观看。

部分内容参考文献
本文作者,引用或转载请注明原文链接

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注