LOADING

加载过慢请开启缓存 浏览器默认开启

FFT/NTT 学习笔记

引入

我们在计算两个多项式的乘积的时候,最常见的做法也就是一位一位相乘,然后加到对应的位置,也就是: \[ f_{x}=\sum_{i+j=x}f_{i}\times f_{j} \] 这也就是乘法卷积。容易发现普通计算是 \(O(n^2)\) 的。可不可以优化呢?

FFT

首先,FFT 全称为:快速傅里叶变换。它的作用是将乘法卷积朴素做法的 \(O(n^2)\),优化为 \(O(n\log{n})\),速度快了不少。
首先,我们来讲解一些前置知识。

复数

引入

初中的时候,我们学过 平方根运算,也就是 \(\sqrt{x}\),我们知道,当 \(x\) 大于等于 \(0\) 时,\(\sqrt{x}\) 才是有意义的。
那我们要是硬要算 \(\sqrt{-1}\) 呢?数学家们定义了一种新的数:虚数。相对应的,我们一般用的数,称为实数,用 \(\mathbb{R}\) 表示。
虚数的单位是 \(\mathrm{i}\),你可能会问。实数的单位是什么,其实是 \(1\)。所以你大概可以将 \(\mathrm{i}\) 当成虚数集的 \(1\)。因为 \(\sqrt{-1}=\mathrm{i}\),所以有 \(\mathrm{i}^{2}=-1\),这点很重要,记住。

虚数是有四则运算的,也就是:

  • \(a\mathrm{i}+b\mathrm{i}=(a+b)\mathrm{i}\)
  • \(a\mathrm{i}-b\mathrm{i}=(a-b)\mathrm{i}\)
  • \(a\mathrm{i}\times b\mathrm{i}=ab\mathrm{i}^{2}=-ab\)
  • \(a\mathrm{i}\div b\mathrm{i}=\dfrac{a}{b}\mathrm{i}\)

复数则是一个实数加上一个虚数,一般表示为 \(a+b\mathrm{i}\)。其中,\(a\) 称为实部\(b\mathrm{i}\) 称为虚部
我们知道,一条数轴可以表示出所有实数,那么我们加上一条与之垂直的数轴,用来表示虚数,那么在这个二维平面上的点,都是一个复数,而这个点的 \(x\) 坐标则是这个复数的实部\(y\) 坐标则是这个复数的虚部。一般用 \(z\) 来表示一个复数。用 \(\mathbb{C}\) 表示复数集。

比如上图中的 B 点,就可以表示 \(3+5i\) 这个复数,我们发现,其实这很像向量,也就是 \(\overrightarrow{OB}=(a,b)\)

复数解

在初中求解一元二次方程的时候,假如方程是 \(ax^{x}+bx+c=d(a\not =0,a,b,c,d\in \mathbb{R})\) 我们有一个 \(\Delta=b^{2}-4ac\),初中老师告诉我们,当 \(\Delta<0\) 时,没有 实数 根。但其实,是有 复数 根的。
比如 \(x^{2}+2x+2=0\) 这个方程。我们得出 \(\Delta=2^{2}-4\times2\times1=-4\),如果我们强行带入求根公式中可以得到: \[ \begin{cases} x_{1}=\dfrac{-b+\sqrt{\Delta}}{2a}=\dfrac{-2+\sqrt{-4}}{2}=-1+\mathrm{i}\\ x_{2}=\dfrac{-b-\sqrt{\Delta}}{2a}=\dfrac{-2-\sqrt{-4}}{2}=-1-\mathrm{i} \end{cases} \] 这两个根,也被称为原方程的复数根,所以,你现在应该能明白为什么初中求解一元二次方程的时候,要说 没有实数根,而不说 没有根 了。

四则运算

复数是满足四则运算的。

  • \((a+c\mathrm{i})+(b+d\mathrm{i})=(a+b)+(c+d)\mathrm{i}\)
  • \((a+c\mathrm{i})-(b+d\mathrm{i})=(a-b)+(c-d)\mathrm{i}\)

容易发现,复数的加减法是满足交换律和结合律的。也就是:

  • \(z_{1}+z_{2}+z_{3}=z_{1}+z_{3}+z_{2}\)
  • \(z_{1}+z_{2}+z_{3}=z_{1}+(z_{2}+z_{3})\)

联系一下向量。我们发现这也同样满足向量的加减法。(也就是四边形法则)

上图表示了 \(z_{1}=3+5\mathrm{i},z_{2}=4-2\mathrm{i}\) 两个复数的加法,我们知道其等于 \(z_{3}=7+3\mathrm{i}\),我们又知道 \(\overrightarrow{OB}+\overrightarrow{OF}=\overrightarrow{OE}\)。而这几个向量又刚好分别对应了 \(z_{1},z_{2},z_{3}\)
减法同理。

在讲乘除法前,我们先引入另一个东西:共轭复数。一个复数 \(z\) 的共轭复数用 \(\overline{z}\) 来表示。具体是什么呢?也就是 \(z\) 的虚部取反,比如 \(z_{1}=3+5\mathrm{i}\)\(\overline{z_{1}}=3-5\mathrm{i}\)
乘法是很简单的:

  • \((a+c\mathrm{i})\times(b+d\mathrm{i})=ab+ad\mathrm{i}+bc\mathrm{i}+cd\mathrm{i}^{2}=(ab-cd)+(ad+bc)\mathrm{i}\)

这个同样是和向量乘法相同的,也就是幅角相加,辐长相乘

上图表示了 \(z_{1}=3+5\mathrm{i},z_{2}=4-2\mathrm{i}\) 两个复数的乘法,我们知道其等于 \(z_{3}=22+14\mathrm{i}\)。我们又知道 \(\overrightarrow{OB}\times\overrightarrow{OF}=\overrightarrow{OK}\)。而这几个向量又刚好分别对应了 \(z_{1},z_{2},z_{3}\)。(\(\alpha+\gamma=\delta,OB\times OF=OK\)

除法相对比较复杂:

  • \(\dfrac{a+c\mathrm{i}}{b+d\mathrm{i}}=\dfrac{(a+c\mathrm{i})(b-d\mathrm{i})}{(b+d\mathrm{i})(b-d\mathrm{i})}=\dfrac{(ab+cd)+(bc-ad)\mathrm{i}}{b^{2}+d^{2}}=\dfrac{ab+cd}{b^{2}+d^{2}}+\dfrac{bc-ad}{b^{2}+d^{2}}\mathrm{i}\)

共轭复数的加入非常的巧妙。因为向量没有除法,所以也就不说了。

单位根

抄自 OI-Wiki:

单位根:称 \(x^n=1\) 在复数意义下的解是 \(n\) 次复根。显然,这样的解有 \(n\) 个,称这 \(n\) 个解都是 \(n\) 次 单位根 或 单位复根(the n-th root of unity)。根据复平面的知识,\(n\) 次单位根把单位圆 \(n\) 等分。

简单点说,我们现在有一个单位圆,我们从 \((1,0)\) 这个点开始,将其等分为 \(n\) 等分,将平面直角坐标系看作复平面,那么等分点对应的复数,也就是对应的 \(n\) 次 单位根 中的一个,也就是 \(x^n=1\) 的所有 复数根 构成的集合。一般用 \(\omega_{n}^{k}\) 表示 \(n\) 次单位根中的第 \(k\) 个。

从上图可以看出,是从 \(\omega_{n}^{0}\) 开始的,而不是从 \(\omega_{n}^{1}\) 开始,\(k\) 是可以大于 \(n\) 的,\(k\) 也可以是负数。

我们可以结合图发现一些性质:

  • \(\omega_{n}^{k}=\omega_{n}^{k+n}\)
  • \(\omega_{n}^{k+\frac{n}{2}}=-\omega_{n}^{k}\)
  • \(\omega^{k}_{n}=\omega^{k}_{\frac{n}{2}}\)
  • \(\sum_{i=0}^{n-1}\omega_{n}^{i}=-1\times[n \bmod 2=1]\)
  • \(\sum_{i=0}^{n-1}(\omega_{n}^{i})^{k}=(-1)^{\gcd(n,k)}\times[n \bmod 2=1]\)

第五点为什么是正确的?
对于 \(0\le i<n,k>0,i,k\in\mathbb{Z}\),我们知道 \(ik\bmod n\) 是有循环节的,而且必定有长度为 \(n\) 的循环节,而最短的循环节则恰好是 \(\dfrac{n}{\gcd(n,k)}\)

那我们怎么求单位根呢?
我们知道,单位根是将单位圆等分后的点。每个点又可以对应一条向量,那么 \(\omega_{n}^{k}\) 对应的向量的幅角也就是 \(\dfrac{k}{n}2\pi\),那么根据简单的三角函数我们可以得到,\(\omega_{n}^{k}\) 对应的点为 \((\cos\dfrac{k}{n}2\pi,\sin\dfrac{k}{n}2\pi)\),根据前文所讲,我们便求出了 \(\omega_{n}^{k}\) 对应的复数 \(\cos\dfrac{k}{n}2\pi+\sin\dfrac{k}{n}2\pi \mathrm{i}\)

系数表示法和点值表示法

我们现在有一个多项式 \(f(x)=x^{2}+5x-3\)。现在 \(f(x)\) 的表示方法则称为系数表示法
我们又知道,两点可以确定一条直线,也就是一次函数;三个点可以确认一个二次函数,那么同理,有 \(n+1\) 个点,就可以表示出一个 \(n\) 次多项式。
比如我现在给你三个点 \(A(-2,-9),B(0,-3),C(1,3)\) 我们会发现,同时过这三个点的二次函数,有且仅有一个,也就是 \(f(x)=x^{2}+5x-3\)

我们要将一个系数表示的多项式,转化为点值表示是非常容易的。直接取 \(x=x_{0},x_{1},\dotsb,x_{n}\) 依次带进原函数计算就行了,我们来算算时间复杂度:

一共要算 \(n+1\) 个点,每次计算代回去要计算 \(n+1\) 次,总的时间复杂度便是 \(O(n^{2})\) 的。

那我们我们现在有了已经转化为点值表示的多项式,我们怎么把它转化为系数表示呢?
我们可以使用一种叫做 拉格朗日插值法 的方法,但是我也不会,重点也不在这里,毕竟我们要讲的是 FFT。但你可以知道的是,这个方法的时间复杂度也是 \(O(n^{2})\) 的。(虽然有个东西叫做 多项式快速插值,但时间复杂度也是 \(O(n\log^{2}{n})\) 的。)

你可能会说了,那这复杂度不还是 \(O(n^2)\) 的吗?和暴力有什么区别,还麻烦的多。你先别急,我们要讲的 FFT,便是优化这一过程的。

快速傅里叶变换(FFT)

FFT 的全称,准确说应该叫做 快速离散傅里叶变换。离散傅里叶变换:DFT。FFT 可以在 \(O(n\log n)\) 的时间复杂度下,将一个 系数表示 的多项式,转化为 点值表示。

我们现在有一个多项式 \(f(x)=a_{0}+a_{1}x+a_{2}x^{2}+a_{3}x^{3}+\dotsb+a_{n-1}x^{n-1}\)
我们现在又有两个多项式: \[ \begin{aligned} g(x)&=(a_{0}+a_{2}x+a_{4}x^{2}+\dots +a_{n-2}x^{\frac{n}{2}})\\ h(x)&=(a_{1}+a_{3}x+a_{5}x^{2}+\dots +a_{n-1}x^{\frac{n}{2}})\\ \end{aligned} \] 可以得到: \[ \begin{aligned} g(x^{2})&=(a_{0}+a_{2}x^{2}+a_{4}x^{4}+\dots +a_{n-2}x^{n-2})\\ h(x^{2})&=(a_{1}+a_{3}x^{2}+a_{5}x^{4}+\dots +a_{n-1}x^{n-2})\\ \end{aligned} \] 我们发现 \(f(x)=g(x^{2})+xh(x^{2})\)
我们要求 \(f(x)\) 的点值表示,我们尝试将 \(n\)\(n\) 次单位根依次代入进去。

\(0\le k<\dfrac{n}{2}\) 时: \[ \begin{aligned} f(x)&=g(x^{2})+xh(x^{2})\\ f(\omega^{k}_{n})&=g(\omega^{2k}_{n})+\omega_{n}^{k}h(\omega^{2k}_{n})\\ &=g(\omega^{k}_{\frac{n}{2}})+\omega_{n}^{k}h(\omega^{k}_{\frac{n}{2}})\\ \end{aligned} \]\(\dfrac{n}{2}\le k<n\) 时: \[ \begin{aligned} f(\omega_{n}^{k})&=g(\omega_{n}^{2k})+\omega_{n}^{k}h(\omega_{n}^{2k})\\ &=g(\omega^{2k-n}_{n}\cdot\omega^{n}_{n})-\omega_{n}^{k-\frac{n}{2}}h(\omega^{2k-n}_{n}\cdot\omega^{n}_{n})\\ &=g(\omega_{\frac{n}{2}}^{k-\frac{n}{2}})-\omega_{n}^{k-\frac{n}{2}}h(\omega_{\frac{n}{2}}^{k-\frac{n}{2}})\\ \end{aligned} \] 我们不妨令 \(p=g(\omega^{k}_{\frac{n}{2}}),q=h(\omega^{k}_{\frac{n}{2}})\),可以得到: \[ f(\omega_{n}^{k})= \begin{cases} p+q,0\le k<\dfrac{n}{2}\\ p-q,\dfrac{n}{2}\le k<n \end{cases} \] 得益于单位根的性质,我们计算 \(f(x)\) 时,只需要计算 \(g(x)\)\(h(x)\) ,而他们都是把问题规模缩小了一半。于是我们便可以一直递归下去处理,直到问题规模为 \(1\) 的时候,直接计算即可,再一路回溯回去即可。
注意因为问题规模一直除以 \(2\),为了防止出现小数,一开始应该将多项式项数补充至 \(2\) 的整数次幂。

根据主定理 \(T(n)=T(\dfrac{n}{2})+O(n)\) 可得时间复杂度为 \(O(n\log n)\)

快速傅里叶逆变换(IFFT)

和 FFT 很像,FFT 的全称准确说应该是:快速离散傅里叶逆变换。而 离散傅里叶逆变换,则是 IDFT。

我们先来尝试证明一个定理:

我们现在有多项式 \(f(x)=\sum_{i=0}^{n-1}a_{i}x^{i}\) 进行 FFT 后的结果:\((\omega_{n}^{0},f(\omega_{n}^{0})),(\omega_{n}^{1},f(\omega_{n}^{1})),\dotsb,(\omega_{n}^{n-1},f(\omega_{n}^{n-1}))\)。把 \(f(\omega_{n}^{0}),f(\omega_{n}^{1}),\dotsb,f(\omega_{n}^{n-1})\) 分别作为系数,组成一个新的多项式 \(g(x)=\sum_{i=0}^{n-1}f(\omega_{n}^{i})x^{i}\)。再取单位根的倒数即 \(\omega^{0}_{n},\omega^{-1}_{n},\dots,\omega^{-(n-1)}_{n}\) 作为 \(x\) 依次代入 \(g(x)\)。将得到的每个 \(g(\omega_{n}^{-k})\) 除以 \(n\) ,得到的值便对应的是原来 \(f(x)\) 的各项系数。

证明大致如下:

我们选择一个 \(\omega_{n}^{-k}(0\le k<n,k\in\mathbb{Z})\) 代入 \(g(x)\),得到: \[ \begin{aligned} g(\omega_{n}^{-k})&=\sum_{i=0}^{n-1}f(\omega_{n}^{i})(\omega_{n}^{-k})^{i}\\ &=\sum_{i=0}^{n-1}\left(\sum_{j=0}^{n-1}a_{j}(\omega_{n}^{i})^{j}\right)(\omega_{n}^{-k})^{i}\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_{j}(\omega_{n}^{i})^{j}(\omega_{n}^{-k})^{i}\\ &=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_{j}(\omega_{n}^{j-k})^{i}\\ &=\sum_{j=0}^{n-1}a_{j}\left(\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}\dfrac{(\omega_{n}^{j})^{i}}{(\omega_{n}^{k})^{i}}\right)\\ \end{aligned} \] 对于 \(j=k\) 的时候,总和为 \(na_{k}\)。对于 \(j\not =k\) 的时候,我们知道 \((\omega_{n}^{j})^{i}=0\) 因此总和也为 \(0\),综上 \(\dfrac{g(\omega_{n}^{-k})}{n}=a_{k}\),得证。

所以说我们现在本质上也是将 插值,转化成了 求值,因此时间复杂度也是 \(O(n\log n)\) 的。

乘起来

我们现在既可以将系数表示法转化为点值表示法,也可以将点值表示法转化为系数表示法,而且都是很快速的 \(O(n\log n)\)。那么我们怎么把两个多项式乘起来呢?

如果我们有两个系数表示的多项式,那么我们就只能像最开始那样挨个挨个枚举,然后乘起来,加起来。复杂度是 \(O(n^2)\) 的。
但我们现在有的是两个点值表示的多项式,我们便只需要将对应的点值乘起来就行了,也就是 \(h_{i}=f_{i}\times g_{i}\)。这个为什么是正确的,你们可以自行思考一下。复杂度显然是 \(O(n)\) 的。

那么我们现在就已经可以在 \(O(n\log n)\) 的时间内完成乘法卷积了!
那么,结束了吗?

优化

我们发现原先是一个递归的过程,那么必然常数较大,那么可以怎么优化呢?

蝶形优化

我们发现递归时转移是这样的。(以 \(n=8\) 为例)

我们要计算下面这些的值: \[ f(\omega^{0}_{8}),f(\omega^{1}_{8}),f(\omega^{2}_{8}),f(\omega^{3}_{8}),f(\omega^{4}_{8}),f(\omega^{5}_{8}),f(\omega^{6}_{8}),f(\omega^{7}_{8}) \] 根据前文,我们会将其分成两组继续计算: \[ f(\omega^{0}_{8}),f(\omega^{2}_{8}),f(\omega^{4}_{8}),f(\omega^{6}_{8})\;\;\;\;f(\omega^{1}_{8}),f(\omega^{3}_{8}),f(\omega^{5}_{8}),f(\omega^{7}_{8}) \] 然后继续拆下去,直到处理的数量为 \(1\)\[ f(\omega^{0}_{8}),f(\omega^{4}_{8})\;\;\;\;f(\omega^{2}_{8}),f(\omega^{6}_{8})\;\;\;\;f(\omega^{1}_{8}),f(\omega^{5}_{8})\;\;\;\;f(\omega^{3}_{8}),f(\omega^{7}_{8}) \] \[ f(\omega^{0}_{8})\;\;\;\;f(\omega^{4}_{8})\;\;\;\;f(\omega^{2}_{8})\;\;\;\;f(\omega^{6}_{8})\;\;\;\;f(\omega^{1}_{8})\;\;\;\;f(\omega^{5}_{8})\;\;\;\;f(\omega^{3}_{8})\;\;\;\;f(\omega^{7}_{8}) \] 最后依次代回去,就可以得出结果。

但我们似乎没找到什么规律呀。我们把编号换成二进制试试? \[ 000,001,010,011,100,101,110,111 \] \[ 000,010,100,110\;\;\;\;001,011,101,111 \] \[ 000,100\;\;\;\;010,110\;\;\;\;001,101\;\;\;\;011,111 \] \[ 000\;\;\;\;100\;\;\;\;010\;\;\;\;110\;\;\;\;001\;\;\;\;101\;\;\;\;011\;\;\;\;111 \] 可能有人发现,最终的位置刚好就是二进制翻转后的值。注意!是翻转,而不是反转
那么怎么进行二进制翻转呢?

二进制翻转

对于每一个数,我们一位一位进行翻转。
但这样的时间复杂度是多少呢?一个数有 \(O(log n)\) 位二进制位,总共 \(O(n)\) 个数,因此时间复杂度是 \(O(n\log n)\),你这不但没有优化常数,反倒有可能增加了常数。

我们不妨令 \(rev(i)\) 表示将数字 \(i\) 在二进制下翻转后的值。 我们在求 \(rev(i)\) 的时候,因为是一个数一个数递推的,因此我们已经算过了 \(rev(\left\lfloor\frac{i}{2}\right\rfloor)\)。我们知道 \(\left\lfloor\dfrac{i}{2}\right\rfloor\) 在代码中,是可以通过将 \(i\) 右移一位得到的。所以说这个数翻转后的值再右移一位,也就刚好是原来的数字 \(i\) 除去最低位后反转的值,而对于最低位,特殊处理一下即可。
没太懂的话,可以看一下下面的图。

实现起来代码也很短:

n=(1<<BIT);
for(int i=0;i<n;i++) rev[i]=(rev[i>>1]>>1)|(i&1<<BIT-1);

那么,因为是递推,所以时间复杂度也就是 \(O(n)\) 的,而且我们可以将原先的计算形式从递归改成递推,常数还能更小。

合并答案

根据 FFT 的过程,我们知道它是从小区间贡献到大的区间 \((2\to 4\to 8\to \dots \to n)\)

所以我们利用区间DP的思想(可以这么说吧)。
回顾先前的式子:

\[ f(\omega_{n}^{k})= \begin{cases} p+q,0\le k<\dfrac{n}{2}\\ p-q,\dfrac{n}{2}\le k<n \end{cases} \]

我们便可以写出代码如下:

for(int k=2;k<=n;k*=2){
    fs omega=fs(cos(2*pi/k),sin(z*2*pi/k));
    for(int i=0;i<n;i+=k){
        fs w=fs(1,0);
        for(int j=i;j<i+k/2;j++){
            fs zs=w*c[j+k/2];
            c[j+k/2]=c[j]-zs;
            c[j]+=zs;
            w*=omega;
        }
    }
}

在进行 IFFT 的时候,因为我们用的是 单位根 的倒数,也就是 \(\omega_{n}^{-k}=\cos\dfrac{k}{n}2\pi-\sin\dfrac{k}{n}2\pi \mathrm{i}\) 可以画图思考一下为什么只需要将 \(\sin\) 的部分变成负的就行了。最后别忘记除以 \(n\)

三次变两次

不会xwx

代码

P3803 【模板】多项式乘法(FFT)

// Problem: P3803 【模板】多项式乘法(FFT)
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3803
// Memory Limit: 500 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
typedef complex<double> fs;
const int MAXN=4e6+10;
const int mod1=1e9+7;
const int mod2=998244353;
const int inf_int=0x7f7f7f7f;
const long long inf_long=0x7f7f7f7f7f7f7f7f;
const double pi=acos(-1);
const double eps=1e-9;
char Buf[1<<23],*P1=Buf,*P2=Buf;
#define getchar() (P1==P2&&(P2=(P1=Buf)+fread(Buf,1,1<<23,stdin),P1==P2)?EOF:*P1++)
template<typename type>
inline void read(type &x){
    x=0;
    bool f=false;
    char ch=getchar();
    while(ch<'0'||ch>'9') f|=ch=='-',ch=getchar();
    while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
    if(f) x=-x;
}
template<typename type,typename... args>
inline void read(type &x,args&... y){
    read(x),read(y...);
}

int N,M,BIT,rev[MAXN];
fs a[MAXN],b[MAXN];

void FFT(fs *s,int opt,int bit){
    int n=(1<<bit);
    for(int i=0;i<n;i++) rev[i]=(rev[i>>1]>>1)|((i&1)<<bit-1);
    for(int i=0;i<n;i++) if(i<rev[i]) swap(s[i],s[rev[i]]);
    for(int i=2;i<=n;i*=2){
        fs omega=fs(cos(2*pi/i),sin(opt*2*pi/i));
        for(int j=0;j<n;j+=i){
            fs z=fs(1,0);
            for(int k=j;k<j+i/2;k++){
                fs p=s[k],q=z*s[k+i/2];
                s[k]=p+q;
                s[k+i/2]=p-q;
                z*=omega;
            }
        }
    }
    if(opt==-1) for(int i=0;i<n;i++) s[i].real(s[i].real()/n);
}

signed main(){
    read(N,M);
    for(int i=0,A;i<=N;i++) read(A),a[i].real(A);
    for(int i=0,B;i<=M;i++) read(B),b[i].real(B);
    while((1<<BIT)<N+M+1) BIT++;
    FFT(a,1,BIT);FFT(b,1,BIT);
    for(int i=0;i<(1<<BIT);i++) a[i]*=b[i];
    FFT(a,-1,BIT);
    for(int i=0;i<=N+M;i++) cout<<(int)(a[i].real()+0.5)<<" ";
    cout<<endl;
    return 0;
}

其实 NTT 大概就是把单位根改成了 模数对应的原根