「学习笔记」FFT 之优化——NTT
「学习笔记」FFT 之优化——NTT
前言
\(NTT\) 在某种意义上说,应该属于 \(FFT\) 的一种优化。
——因而必备知识肯定要有 \(FFT\) 啦...
如果不知道 \(FFT\) 的大佬可以走这里
引入
在 \(FFT\) 中,为了能计算单位原根 \(\omega\) ,我们使用了 \(\text{C++}\) 的 math
库中的 \(cos、sin\) 函数,所以我们无法避免地使用了 double
以及其运算。
但是,众所周知的, double
的运算很慢,并且,我们的虚数乘法是类似于下面这种打法:
cplx operator * (const cplx a)const{return cplx(vr*a.vr-vi*a.vi,vr*a.vi+a.vr*vi);}
显然,一次虚数乘法涉及四次 double
的乘法。
并且在运算过程中,会有大量的精度丢失,这都是我们不可接受的。
然而问题来了:我们多项式乘法都是整数在那里搞来搞去,为什么一定要扯到浮点数。是否存在一个在模意义下的,只使用整数的方法?——Tiw_Air_OAO
快速数论变换——NTT
想一想我们使用了单位复根的哪些特性:
- \(w_{n}^{i}*w_{n}^{j}=w_{n}^{i+j}\)
- \(w_{dn}^{dk}=w_n^k\)
- \(w_{2n}^k=-w_{2n}^{k+n}\)
- \(n\) 个单位根互不相同,且 \(w_n^0=1\)
那么我们能否在 模意义 下找到一个性质相同的数?
这里有一个同样也是 某某根 的东西,叫做 原根 。
对于素数 \(p\) , \(p\) 的原根 \(G\) 定义为使得 \(G^0,G^1,...,G^{p−2}(mod\space p)\) 互不相同的数。
仔细思考一下,发现 原根 与 单位复根 很像。
同理,我们再定义 \(g_n^k = (G^{\frac{p-1}{n}})^k\) ,这样 \(g_n^k\) 就与 \(\omega_n^k\) 长得更像了...
但是,必须在 \(g_n^k\) 满足与 \(\omega_n^k\) 同样的性质时,我们才能等价替换。
现在,我们检验原根在模意义下是否满足与单位复根同样的性质:
- 由幂的运算立即可得
- 由幂的运算立即可得
- \(g_{2n}^{k+n}=(G^{\frac{p-1}{2n}})^{k+n}=(G^{\frac{p-1}{2n}})^k*(G^{\frac{p-1}{2n}})^n=G^{\frac{p-1}{2}}*g_{2n}^k=-g_{2n}^k(mod\space p)\) ,因为 \((G^{p-1}=1(mod\space p)\) 且由原根定义 \(G^{\frac{p-1}{2}}\not=G^{p-1}(mod\space p)\) ,故 \(G^{\frac{p-1}{2}}=-1(mod\space p)\)
- 由原根的定义立即可得
发现原根可以在模意义下 完全替换 单位复根。
这就是 \(NTT\) 了。
但是,这样的方法对模数会有一定的限制
令 \(m = 2^p*k+1\) , \(k\) 为奇数,则多项式长度必须 \(n \le 2^p\)
至于模数以及其原根,没有必要来死记,为什么?
我们程序员就应该干我们经常干的事情——打表可得...
以下是参考代码:
#include<cstdio>
#include<algorithm>
using namespace std;
#define rep(i,__l,__r) for(register int i=__l,i##_end_=__r;i<=i##_end_;++i)
#define fep(i,__l,__r) for(register int i=__l,i##_end_=__r;i>=i##_end_;--i)
#define writc(a,b) fwrit(a),putchar(b)
#define mp(a,b) make_pair(a,b)
#define ft first
#define sd second
#define LL long long
#define ull unsigned long long
#define pii pair<int,int>
#define Endl putchar('\n')
// #define FILEOI
// #define int long long
#ifdef FILEOI
#define MAXBUFFERSIZE 500000
inline char fgetc(){
static char buf[MAXBUFFERSIZE+5],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,MAXBUFFERSIZE,stdin),p1==p2)?EOF:*p1++;
}
#undef MAXBUFFERSIZE
#define cg (c=fgetc())
#else
#define cg (c=getchar())
#endif
template<class T>inline void qread(T& x){
char c;bool f=0;
while(cg<'0'||'9'<c)f|=(c=='-');
for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
if(f)x=-x;
}
inline int qread(){
int x=0;char c;bool f=0;
while(cg<'0'||'9'<c)f|=(c=='-');
for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
return f?-x:x;
}
template<class T,class... Args>inline void qread(T& x,Args&... args){qread(x),qread(args...);}
template<class T>inline T Max(const T x,const T y){return x>y?x:y;}
template<class T>inline T Min(const T x,const T y){return x<y?x:y;}
template<class T>inline T fab(const T x){return x>0?x:-x;}
inline int gcd(const int a,const int b){return b?gcd(b,a%b):a;}
inline void getInv(int inv[],const int lim,const int MOD){
inv[0]=inv[1]=1;for(int i=2;i<=lim;++i)inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
}
template<class T>void fwrit(const T x){
if(x<0)return (void)(putchar('-'),fwrit(-x));
if(x>9)fwrit(x/10);putchar(x%10^48);
}
inline LL mulMod(const LL a,const LL b,const LL mod){//long long multiplie_mod
return ((a*b-(LL)((long double)a/mod*b+1e-8)*mod)%mod+mod)%mod;
}
const int MAXN=3e6;
const int MOD=998244353,g=3,gi=332748118;
int n,m;
int a[MAXN+5],b[MAXN+5],revi[MAXN+5];
inline int qkpow(int a,int x){
int ret=1;
for(;x>0;x>>=1){
if(x&1)ret=1ll*ret*a%MOD;
a=1ll*a*a%MOD;
}
return ret;
}
inline void ntt(int* f,const short opt=1){
for(int i=0;i<n;++i)if(i<revi[i])swap(f[i],f[revi[i]]);
for(int p=2,len,gn,Pow,tmp;p<=n;p<<=1){
len=p>>1,gn=qkpow(opt==1?g:gi,(MOD-1)/p);
for(int k=0;k<n;k+=p){Pow=1;
for(int l=k;l<k+len;++l,Pow=1ll*Pow*gn%MOD){
tmp=1ll*Pow*f[len+l]%MOD;
if(f[l]-tmp<0)f[len+l]=f[l]-tmp+MOD;
else f[len+l]=f[l]-tmp;
if(f[l]-MOD+tmp>0)f[l]=f[l]-MOD+tmp;
else f[l]+=tmp;
}
}
}
if(opt==-1){
int inv=qkpow(n,MOD-2);
for(int i=0;i<n;++i)f[i]=1ll*f[i]*inv%MOD;
}
}
inline void launch(){
qread(n,m);
rep(i,0,n)qread(a[i]);
rep(i,0,m)qread(b[i]);
for(m+=n,n=1;n<=m;n<<=1);
for(int i=0;i<n;++i)revi[i]=(revi[i>>1]>>1)|((i&1)?n>>1:0);
ntt(a),ntt(b);
for(int i=0;i<n;++i)a[i]=1ll*a[i]*b[i]%MOD;
ntt(a,-1);
rep(i,0,m)writc(a[i],' ');
Endl;
}
signed main(){
#ifdef FILEOI
freopen("file.in","r",stdin);
freopen("file.out","w",stdout);
#endif
launch();
return 0;
}
一些引申问题及解决方法
假如题目中规定了模数怎么办?还卡 FFT 的精度怎么办?
有两种方法:三模数 NTT 以及 拆系数 FFT (MTT)
三模数 NTT
我们可以选取三个适用于 \(NTT\) 的模数 \(M1,M2,M3\) 进行 \(NTT\) ,用中国剩余定理合并得到 \(x\space mod\space (M1*M2*M3)\) 的值。只要保证 \(x < M1*M2*M3\) 就可以直接输出这个值。
之所以是三模数,因为用三个大小在 \(10^9\) 左右模数对于大部分题目来说就足够了。
但是 \(M1*M2*M3\) 可能非常大怎么办呢?难不成我还要写高精度?其实也可以。
我们列出同余方程组:
x \equiv a_1&\mod m_1\\
x \equiv a_2&\mod m_2\\
x \equiv a_3&\mod m_3\\
\end{cases}
\]
用中国剩余定理合并前两个方程组,得到:
x \equiv A&\mod M\\
x \equiv a_3&\mod m_3\\
\end{cases}
\]
其中的 \(M\) 满足 \(M = m1*m2 < 10^{18}\)
然后将第一个方程变形得到 \(x = kM + A\) ,代入第二个方程,得到:
k \equiv (a_3-A)*M^{-1} \mod m_3\\
\]
令 \(Q = (a_3-A)*M^{-1}\) ,则 \(k = Pm_3 + Q\) 。
再将上式代入回 \(x = kM + A\) ,得 \(x = (Pm_3 + Q)M+ A = Pm_3M+QM+A\) 。
又因为 \(M = m_1m_2\) ,所以 \(x = Pm_1m_2m_3 + QM + A\) 。
也就是说 \(x \equiv QM + A \mod m_1m_2m_3\) 。
然后,我们完美地解决了这个东西。
接下来是代码:
#include<cstdio>
#include<algorithm>
using namespace std;
#define rep(i,__l,__r) for(register int i=__l,i##_end_=__r;i<=i##_end_;++i)
#define fep(i,__l,__r) for(register int i=__l,i##_end_=__r;i>=i##_end_;--i)
#define writc(a,b) fwrit(a),putchar(b)
#define mp(a,b) make_pair(a,b)
#define ft first
#define sd second
#define LL long long
#define ull unsigned long long
#define pii pair<int,int>
#define Endl putchar('\n')
// #define FILEOI
#define int long long
#ifdef FILEOI
#define MAXBUFFERSIZE 500000
inline char fgetc(){
static char buf[MAXBUFFERSIZE+5],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,MAXBUFFERSIZE,stdin),p1==p2)?EOF:*p1++;
}
#undef MAXBUFFERSIZE
#define cg (c=fgetc())
#else
#define cg (c=getchar())
#endif
template<class T>inline void qread(T& x){
char c;bool f=0;
while(cg<'0'||'9'<c)f|=(c=='-');
for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
if(f)x=-x;
}
inline int qread(){
int x=0;char c;bool f=0;
while(cg<'0'||'9'<c)f|=(c=='-');
for(x=(c^48);'0'<=cg&&c<='9';x=(x<<1)+(x<<3)+(c^48));
return f?-x:x;
}
template<class T,class... Args>inline void qread(T& x,Args&... args){qread(x),qread(args...);}
template<class T>inline T Max(const T x,const T y){return x>y?x:y;}
template<class T>inline T Min(const T x,const T y){return x<y?x:y;}
template<class T>inline T fab(const T x){return x>0?x:-x;}
inline int gcd(const int a,const int b){return b?gcd(b,a%b):a;}
inline void getInv(int inv[],const int lim,const int MOD){
inv[0]=inv[1]=1;for(int i=2;i<=lim;++i)inv[i]=1ll*inv[MOD%i]*(MOD-MOD/i)%MOD;
}
template<class T>void fwrit(const T x){
if(x<0)return (void)(putchar('-'),fwrit(-x));
if(x>9)fwrit(x/10);putchar(x%10^48);
}
inline LL mulMod(const LL a,const LL b,const LL mod){//long long multiplie_mod
return ((a*b-(LL)((long double)a/mod*b+1e-8)*mod)%mod+mod)%mod;
}
inline int qkpow(int a,int n,const int mod){
int ret=1;
for(;n>0;n>>=1){
if(n&1)ret=ret*a%mod;
a=a*a%mod;
}
return ret;
}
const int MAXN=3e5;
const int MOD[3]={469762049ll,998244353ll,1004535809ll};//三模数
const int G=3;//共用的原根
int inv[3][3],k1,k2,Inv,M;
//inv[i][j] : MOD[i] 在 (mod MOD[j]) 下的逆元
int h[3][MAXN+5],g[3][MAXN+5];
//h/g[i][j] : 原函数的第 j 位在 (mod MOD[i]) 的情况下的值
int revi[MAXN+5];//反转数组
inline void init(){
rep(i,0,2)rep(j,0,2)if(i!=j)//处理 inv 数组, 主要用到费马小定理
inv[i][j]=qkpow(MOD[i],MOD[j]-2,MOD[j]);
M=MOD[0]*MOD[1];
k1=mulMod(MOD[1],inv[1][0],M);
k2=mulMod(MOD[0],inv[0][1],M);
Inv=inv[0][2]*inv[1][2]%MOD[2];
}
inline int crt(const int a1,const int a2,const int a3,const int mod){
int A=(mulMod(a1,k1,M)+mulMod(a2,k2,M))%M;
int K=(a3+MOD[2]-A%MOD[2])%MOD[2]*Inv%MOD[2];
return ((M%mod)*K%mod+A)%mod;
}
inline void ntt(int* f,const int n,const int m,const short opt=1){
/*
和普通的 ntt 没啥区别, 如果有什么问题, 可以去查查 fft 的资料
唯一有区别的地方在于取模的时候, 要根据我们目前计算的模数下的运算来取模
*/
for(int i=0;i<n;++i)if(i<revi[i])swap(f[i],f[revi[i]]);
for(int s=2;s<=n;s<<=1){
int t=s>>1,u=(opt==-1)?qkpow(G,(MOD[m]-1)/s,MOD[m]):qkpow(G,MOD[m]-1-(MOD[m]-1)/s,MOD[m]);
for(int i=0;i<n;i+=s){int w=1;
for(int j=i;j<i+t;++j,w=w*u%MOD[m]){
int x=f[j],y=w*f[j+t]%MOD[m];
f[j]=(x+y)%MOD[m];
f[j+t]=(x-y+MOD[m])%MOD[m];
}
}
}
if(opt==-1){
int inv=qkpow(n,MOD[m]-2,MOD[m]);
rep(i,0,n-1)f[i]=f[i]*inv%MOD[m];
}
}
int n,m,p;
inline void launch(){
init();
qread(n,m,p);
rep(i,0,n){//这里我输入的最大的一个模数, 因为其已经超过 1e9 的范围, 刚刚输入时不用取模
qread(h[2][i]);
h[1][i]=h[2][i]%MOD[1];
h[0][i]=h[2][i]%MOD[0];
}
rep(i,0,m){
qread(g[2][i]);
g[1][i]=g[2][i]%MOD[1];
g[0][i]=g[2][i]%MOD[0];
}
for(m+=n,n=1;n<=m;n<<=1);
for(int i=0;i<n;++i)revi[i]=(revi[i>>1]>>1)|((i&1)?n>>1:0);
rep(i,0,2){
ntt(h[i],n,i),ntt(g[i],n,i);
rep(j,0,n-1)h[i][j]=h[i][j]*g[i][j]%MOD[i];
ntt(h[i],n,i,-1);
}
for(int i=0;i<=m;++i)
writc(crt(h[0][i],h[1][i],h[2][i],p),' ');
//使用 crt(我国剩余定理) 来还原答案
// rep(i,0,m)printf("%lld %lld %lld\n",h[0][i],h[1][i],h[2][i]);
}
signed main(){
#ifdef FILEOI
freopen("file.in","r",stdin);
freopen("file.out","w",stdout);
#endif
launch();
return 0;
}
拆系数 FFT (MTT)
我太菜了,还不会...等我更新吧...
「学习笔记」FFT 之优化——NTT的更多相关文章
- 「学习笔记」FFT 快速傅里叶变换
目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...
- 「学习笔记」FFT及NTT入门知识
前言 快速傅里叶变换(\(\text{Fast Fourier Transform,FFT}\) )是一种能在\(O(n \log n)\)的时间内完成多项式乘法的算法,在\(OI\)中的应用很多,是 ...
- 「学习笔记」单调队列优化dp
目录 算法 例题 最大子段和 题意 思路 代码 修剪草坪 题意 思路 代码 瑰丽华尔兹 题意 思路 代码 股票交易 题意 思路 代码 算法 使用单调队列优化dp 废话 对与一些dp的转移方程,我们可以 ...
- 「学习笔记」字符串基础:Hash,KMP与Trie
「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...
- 「学习笔记」Min25筛
「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...
- 「学习笔记」Treap
「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...
- 「学习笔记」wqs二分/dp凸优化
[学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...
- 「学习笔记」斜率优化dp
目录 算法 例题 任务安排 题意 思路 代码 [SDOI2012]任务安排 题意 思路 代码 任务安排 再改 题意 思路 练习题 [HNOI2008]玩具装箱 思路 代码 [APIO2010]特别行动 ...
- 「学习笔记」递推 & 递归
引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...
随机推荐
- 主席树+二分 p4602
题意:给出每一种果汁的美味度,价格,升数: m个询问,每个询问给出最高上限的钱g,以及给出最少的w 意思是,最多用g的钱去买最少l的果汁,问能得到的最大美味度: 美味度是取所有果汁中美味度的最小值: ...
- HDU 2586(LCA欧拉序和st表)
什么是欧拉序,可以去这个大佬的博客(https://www.cnblogs.com/stxy-ferryman/p/7741970.html)巨详细 因为欧拉序中的两点之间,就是两点遍历的过程,所以只 ...
- 微服务介绍和springCloud组件
微服务架构模式是:将整个web服务 组织成一系列小的web 服务,这些小的web服务可以进行独立的编译和部署,并通过各自暴露的API接口 进行相互通信,他们相互协作,作为一个整体,为客户提供服务功 ...
- mui 进度条 隐藏
官方提供的 mui(contanier).progressbar().hide(); 并未起作用,DOM是js动态添加的,结果无法隐藏.(越使用,mui 的坑越多,陆续记录中...) 后使用下面的方法 ...
- itest(爱测试) 4.3.0 发布,开源BUG 跟踪管理 & 敏捷测试管理软件
itest 简介:查看简介 test 开源敏捷测试管理,testOps 践行者.可按测试包分配测试用例执行,也可建测试迭代(含任务,测试包,BUG)来组织测试工作,也有测试环境管理,还有很常用的测试度 ...
- [踩坑记录] runtime error: load of null pointer of type 'const int' (leetcode)
leetcode上面做题遇到的错误 原因: 在调用函数时,如果返回值如果是一个常量则没问题.如果返回值若为指针则可能会出现该错误,假如返回的指针地址指向函数内的局部变量,在函数退出时,该变量的存储空间 ...
- 科技股 - 5G、芯片、半导体 细分龙头
5G.芯片.半导体 细分龙头 来源:头条-南山话投资 1.射频芯片:卓胜微 2.存储芯片设计:兆易创新 3.GPU:景嘉微 4.模拟电路芯片:圣邦股份 5.半导体分立器件:扬杰科技 6.晶圆代工:中芯 ...
- kettel路径配置
背景 kettel 8.3 jdk13.0.1 jre1.8.0 配置 PENTAHO_JAVA_HOME:C:\Program Files (x86)\Java\jre1.8.0_241 JAVA_ ...
- Celery异步框架
一.什么是celery Celery是一个简单.灵活且可靠的,处理大量消息的分布式系统 专注于实时处理的异步任务队列 同时也支持任务调度 二.Celery架构 Celery的架构由三部分组成,消息中间 ...
- 安卓之线性布局LinearLayout
一.xml属性 (1)orientation:指定线性布局的方向 (2)gravity:指定布局内部视图与本线性布局的对齐方式 (3)layout_weight:指定当前视图的宽或高占上级 ...