快速数论变换NTT学习笔记

发布时间 2023-07-02 20:52:25作者: DengDuck

前言

这篇文章中我介绍了什么是 \(\text{FFT}\) ,但是在文中我们也说了一嘴这玩意是有精度误差的,三角函数和复数导致我们不得不进行取整操作。

只要毒瘤出题人原因,那就可以 \(\text{Hack FFT}\)

Tips:

根据《NOI大纲》的内容,卡精度和卡常数通常是不允许的。

所以 \(\text{FFT}\) 通常不会寄,不必过度焦虑。

但是 \(\text{NTT}\) 本身并不难。

这时候我们就需要引入快速数论变换 \(\text{NTT}\)(\(\text{Fast Number Theory Transform}\),不过我们通常省略那个 \(\text F\))。

首先我们要明确一个方向,就是 \(\text{FFT}\) 的原理是单位根的几个性质:

  • 消去原理: \(\omega_{tn}^{tk}=\omega_{n}^k\)
  • 对称原理:\(\omega_{n}^{k}=-\omega_n^{k+\frac n 2}\)
  • \(\omega_{n}^k=(\omega_n^1)^k\)
  • \(\omega_n^i\not=\omega_n^j(i\not=j)\)

也就是说,只要满足以上条件,也就可以用类似的方法实现。

我们发现,数论里的原根,和这个很像啊!所以直接使用即可。

代码实现

经典的模数 \(998244353\) 的原根为 \(3\),这是常用的组合。

直接替换 \(\omega\) 即可。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const LL N=1e6;
const LL mod=998244353;
const LL G=3;
LL ksm(LL x,LL y)
{
	LL ans=1;
	while(y)
	{
		if(y&1)ans=ans*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return ans;
}
LL n,m,Gi,lim,len=1,r[N],x,ans[N];
LL a[N],b[N];
void FFT(LL *a,LL inv)
{
	for(int i=0;i<=len-1;i++)
	{
		if(i<r[i])swap(a[i],a[r[i]]);
	}
	for(int l=2;l<=len;l*=2)
	{
		LL omg=ksm(G,(mod-1)/l);
		if(inv)omg=ksm(Gi,(mod-1)/l);
		LL m=l/2;
		for(int j=0;j<=len-1;j+=l)
		{
			LL x=1;
			for(int i=0;i<=m-1;i++)
			{
				LL t=x;
				t=a[i+j+m]*t%mod;
				a[i+j+m]=(a[i+j]-t+mod)%mod,a[i+j]=(a[i+j]+t)%mod;
				x=x*omg%mod;
			}
		}
	}
}
int main()
{
	scanf("%lld%lld",&n,&m);
	Gi=ksm(G,mod-2);
	while(len<=n+m)len*=2,lim++;
	for(int i=0;i<=len-1;i++)
	{
		LL sum=0;
		for(int j=0;j<=lim-1;j++)sum|=((i>>j)&1)<<(lim-j-1);
		r[i]=sum;
	}
	for(int i=0;i<=n;i++)
	{
		scanf("%lld",&a[i]);
	}
	for(int i=0;i<=m;i++)
	{
		scanf("%lld",&b[i]);
	}
	FFT(a,0),FFT(b,0);
	for(int i=0;i<=len-1;i++)
	{
		a[i]=a[i]*b[i]%mod;
	}
	FFT(a,1);
	LL inv=ksm(len,mod-2);
	for(int i=0;i<=n+m;i++)
	{
		ans[i]=a[i]*inv%mod;
		printf("%lld ",ans[i]);
	}
	return 0;
}

扩展

通常情况下在模意义都会使用原根来替换单位根,比如单位根反演就可以使用原根。

这里简单展示一下单位根反演的结论:

\[[n|k]=\frac 1 n \sum_{i=0}^{n-1}\omega_{n}^{ki} \]