• NTT学习笔记


    粗略的学习笔记~

    NTT(快速数论变换)

    • 解决多项式卷积在取模意义下的值,并且不使用复数(浮点数)而是使用整数(在取模的意义下)来减少精度误差。

    其实FFT预处理单位根和将较大数据拆成两部分也可以减少精度误差,但是还是没有NTT的精度高,而且不方便取模。


    这里假设你会了FFT。
    不会的话看这里FFT

    FFT之所以精度误差大,就是在于单位根为无理数ee的多少次方,所以必须用浮点数,那么由于计算机的特性,误差会很大。

    而这里是在取模的一个前提下,那么如果我们找到了一个新的且为整数的单位根(这里是在取模意义下),我们记其为GG,那么用这个根去替代原来的ee,进行运算。由于所有运算就会变成整数运算,所以精度误差大大降低。

    如何找到这么一个神奇GG满足原来的单位根的性质呢?


    原根

    原根:对于一个数字mm,当gcd(a,m)=1gcd(a,m)=1aamm的最大公约数为1,也就是互质)时,我们记满足ax1(mod m)a^xequiv 1( m mod m)式子的最小xxordm(a)ord_m(a),然后当ordm(a)=φ(m)ord_m(a)=varphi(m)时称aamm的原根。(不一定所有的数字都有原根)。

    扩展:
    费马小定理:ap11(mod p)a^{p-1}equiv 1( m mod p)pp为素数。(这个可以用来求逆元,也可写为aφ(p)1(mod p)a^{varphi(p)}equiv 1( m mod p)因为φ(p)=p1varphi(p)=p-1pp为质数)
    费马小定理还有个用途就是当幂的指数特别大时,需要对pp取模,我们可以将指数对pp取模然后求(因为根据费马小定理,将模出来等于一的可以先模掉)。
    离散对数:满足axk(mod m)a^xequiv k( m mod m)我们将xx称为aa在模mm的意义下的离散对数,我们可以简记为x=inda(k)x=ind_a(k)

    性质(我们将原根一般用gg表示):

    1. gimod m(i[0,m1])g^i m mod m(iin[0,m-1])可以将[0,m1][0,m-1]内的数字全部取到。
    2. (gm1pimod m)1(g^{frac{m-1}{p_i}} m mod m)≠1,其中pip_imm的素数因子。

    • 原根的求法:
      因为一般原根很小,我们采用暴力枚举的方法。
      对于一个数字mm我们求它的原根,根据性质2,我们先线性筛出mm的所有素数因子,然后从22开始枚举原根gg,如果对于一个枚举的gg,满足对于mm的所有质因数pip_i,满足性质2,那么这个数就是原根,然后break掉就好了。

    要求原根的一个题目BZOJ3992


    转换

    通过离散对数的定义,我们可以将原来FFT单位根的方程xn=1x^n=1写为xn1(mod m)x^nequiv 1( m mod m),那么我们求的新的整数xx不就是原根了吗?

    所以我们用gg来代替了单位根,那么FFTωnk=gk(m1)n(kZ,)我们仍旧学FFT一样记omega_n^k=g^{frac{k(m-1)}{n}}(kin Z,)mm为模数,这样就代替了单位复数根,但是只有当mm为素数时,原根才一定存在,所以我们取模数一般取素数,而且这种质数一般取费马质数(NTT质数)。

    一些常用的取模素数以及原根:IN There


    但是模数并不总是素数,而是一些普通的数字,那么我们怎么办呢?

    那么对于模数,我们可以用几个乘积大于最大卷积的系数的模数,分别求取取模后的卷积,然后用中国剩余定理合并即可。

    不会中国剩余定理的->百度

    但是有时卷积的系数会非常之大,导致你不能直接合并,否则爆long long,所以我们要使用一个小技巧:

    假如我们有三个模数m1,m2,m3m_1,m_2,m_3,其中m1×m2m_1 imes m_2不会爆long long,但是m1×m2×m3m_1 imes m_2 imes m_3会爆,那么我们先直接合并前两个得到M=m1×m2M=m_1 imes m_2的卷积,然后我们就会剩下以下这个方程式:

    {xa1 (mod M)xa2 (mod m3)egin{cases} x equiv a_1 ({mod} M) \ xequiv a_2 ({mod} m_3) end{cases}

    那么转换一下就可以写成:

    {x=a1+k1×Mx=a3+k3×m3egin{cases} x = a_1+k_1 imes M \ x= a_3+k_3 imes m_3 end{cases}

    然后可以写成:

    x=k1×M+a1=k3×m3+a3x=k_1 imes M+a_1=k_3 imes m_3+a_3

    那么:

    k1×Ma3x(mod m3)k_1 imes Mequiv a_3-x( m mod m_3)

    k3×m3k_3 imes m_3m3m_3模掉了。

    所以我们解出k1k_1,然后带入原式即可算出答案xx,那么这个问题就解决了。

    最后我们求出的是在mod(m1×m2×m3) m mod(m_1 imes m_2 imes m_3)的意义下的卷积,如果原来让你mod P(P<(m1×m2×m3)) m mod P(P<(m_1 imes m_2 imes m_3)),那么你再将答案对PP取模即可。


    模板代码:

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define ll long long
    using namespace std;
    
    const int M=3e6+10;
    const ll p1=469762049ll,p2=998244353ll,p3=1004535809ll,g=3;
    const ll Mod=468937312667959297ll;
    //三个乘积大于模数且大于最大卷积的系数P*(n-1)^2,原根均为3
    int n,m,p;
    ll t1[M],t2[M],t3[M],t4[M],ans[3][M];
    int R[M];
    int fpow(int a,int b,int mod){
    	int ans=1;
    	for(;b;b>>=1,a=(1ll*a*a)%mod){
    		if(b&1) ans=(1ll*ans*a)%mod;
    	}
    	return ans;
    }
    ll mul(ll a,ll b,ll mod){
        a%=mod,b%=mod;
        return ((a*b-(ll)((ll)((long double)a/mod*b+1e-3)*mod))%mod+mod)%mod;
    }
    
    void DNT(ll *a,int n,int f,int mod){
    	for(int i=0;i<n;i++) if(i<R[i]) swap(a[i],a[R[i]]);
    	for(int i=2;i<=n;i<<=1){
    		int now=i>>1;
    		int wn=fpow(g,(mod-1)/i,mod);
    		if(f==-1) wn=fpow(wn,mod-2,mod);//逆变换时,逆元,或者采用如下的交换法
    		for(int j=0;j<n;j+=i){
    			int w=1,x,y;
    			for(int k=j;k<j+now;k++,w=1ll*w*wn%mod){
    				x=a[k],y=1ll*w*a[k+now]%mod;
    				a[k]=(x+y)%mod;a[k+now]=(x-y+mod)%mod;
    			}
    		}
    	}
    	if(f==-1){
    		int inv=fpow(n,mod-2,mod);
    		for(int i=0;i<=n;i++){
    			a[i]=1ll*a[i]*inv%mod;
    		}
    //		for(int i=1;i<=n/2;i++) swap(a[i],a[n-i]);//交换法,因为g^(-1)=g^(n-1),所以可以先正变换,翻转就为逆变换。
    	}
    }
    
    void NTT(ll *a,ll *b,int k,int mod){
    	DNT(a,k,1,mod),DNT(b,k,1,mod);
    	for(int i=0;i<=k;i++) a[i]=1ll*a[i]*b[i]%mod;
    //	DNT(a,k,-1,mod);
    }
    int k;
    void mcpy(int d){
    	for(int i=0;i<=k;i++) ans[d][i]=t3[i];
    	if(d==2) return;
    	memset(t3,0,sizeof(t3));memset(t4,0,sizeof(t4));
    	for(int i=0;i<=n;i++) t3[i]=t1[i];
    	for(int i=0;i<=m;i++) t4[i]=t2[i];
    }
    
    void REV(){
    	DNT(ans[0],k,-1,p1);
    	DNT(ans[1],k,-1,p2);
    	DNT(ans[2],k,-1,p3);
    }
    
    int lg;
    int main(){
    	scanf("%d%d%d",&n,&m,&p);
    	for(int i=0;i<=n;i++) scanf("%lld",&t1[i]),t3[i]=t1[i];
    	for(int i=0;i<=m;i++) scanf("%lld",&t2[i]),t4[i]=t2[i];
    	k=1;while(k<=n+m)k<<=1,++lg;
    	for(int i=0;i<k;i++) R[i]=(R[i>>1]>>1)|((i&1)<<(lg-1));
    	NTT(t3,t4,k,p1);
    	mcpy(0);
    	NTT(t3,t4,k,p2);
    	mcpy(1);
    	NTT(t3,t4,k,p3);
    	mcpy(2);
    	REV();
    	for(int i=0;i<=n+m;i++){//中国剩余定理合并+后面推导出的公式
    		ll x=((mul(1ll*ans[0][i]*p2%Mod,fpow(p2%p1,p1-2,p1),Mod))+
    			  (mul(1ll*ans[1][i]*p1%Mod,fpow(p1%p2,p2-2,p2),Mod)))%Mod;
    		ll y=((((ans[2][i]-x)%p3+p3)%p3)*fpow(Mod%p3,p3-2,p3))%p3;
    //		printf("%lld %lld %lld ",x,y,ans[2][i]);
    		printf("%lld ",(1ll*(y%p)*(Mod%p)%p+x%p)%p);
    	}
    	return 0;
    }
    

    一些例题

    【模板】任意模数NTT

    HDU5829 Rikka with Subset - 题解

    [SDOI2015]序列统计


    End

    目前初学,还有很多不懂,望大佬指点错误!


  • 相关阅读:
    Hdu1711 Number Sequence--Kmp模板题
    Trie入门--Poj3630 Phone List,查单词,HDU1251 统计前缀,PKU2503 Babelfish
    高次幂的组合数表示形式
    BZOJ1697 [Usaco2007 Feb] Cow Sorting牛排序
    1025 [SCOI2009]游戏(置换群,DP)
    Poj1721 Cards
    [Poi2003]Shuffle
    poj 3128 Leonardo's Notebook(置换的幂)
    POJ3734 Block母函数入门
    重心拉格朗日插值法
  • 原文地址:https://www.cnblogs.com/VictoryCzt/p/10053431.html
Copyright © 2020-2023  润新知