• 【BZOJ3992】[SDOI2015] 序列统计(原根+NTT+倍增)


    点此看题面

    大致题意: 给定一个所有元素都为小于(m)的非负整数的无重复元素的集合(S)。求有多少个不同的长度为(n)的序列,满足其中每个元素都属于集合(S),且所有元素之积模(m)的值为给定值(x)

    前言

    亡在了数学上。。。

    前面的式子我都推出来了啊,乘法转化成加法的套路也不是没见过,可不知道原根这种用法我真的还是太弱了。

    说起来原根这东西我也挺熟悉的——写(NTT)每次都要用的啊,可我这种写(FFT/NTT)从没搞懂过原理只会背板子的蒟蒻,根本未曾想过原根究竟是个啥玩意。

    现在是明白了,难怪(NTT)必须要用原根,原来就是利用了原根的性质。

    原根

    什么是原根?

    简单的说,对于一个数(p),若(g)满足(g^0 mod p,g^1 mod p,g^2 mod p,...,g^{p-2} mod p)恰好是(1sim p-1)的一个排列,那么(g)就是(p)的原根。

    那么如何求原根呢?很暴力,枚举+验证,只不过验证是需要点技巧的。

    我们可以求出(phi(p))(此题为质数,因此(phi(p)=p-1))的所有质因数(P_i),若存在(P_i)使得(x^{frac{phi(p)}{P_i}}equiv1(mod p)),则(x)不是(p)的原根,否则(x)就是(p)的原根。

    那么原根在这道题中具体有什么用呢?

    等到后面就知道了。

    大致想法

    如果我们设(g_{i,j})长度为(i)的序列中元素之积模(m)(j)的方案数,则显然有转移:

    [g_{x+y,a}=sum_{b imes cequiv a(mod m)}g_{x,b} imes g_{y,c} ]

    显而易见这是一个多项式乘法,但转移条件是(b imes c)十分麻烦,因此我们就想到把乘法转化为加法,然后就能用(NTT)了。

    而怎么把乘法转化为加法呢?

    当然是用对数啦!

    然后我们的原根就闪亮登场了,正因为它的性质,(log_gx=y)(x,y)是一一对应的。

    例如在上面的式子中,我们分别设(w=log_ga,u=log_gb,v=log_gc),就可以得到:

    [g_{x+y,w}=sum_{u+vequiv w(mod m-1)}g_{x,u} imes g_{y,v} ]

    而加法的取模只要在(NTT)做完后把后面的给加到前面去就好了,不用多说。

    这样一来,我们考虑倍增,用(f_{i,j})表示长度为(2^i)的序列中元素之积模(m)(j)的方案数,然后就可以轻松搞定了。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define Con const
    #define CI Con int&
    #define I inline
    #define W while
    #define LN 30
    #define M 8000
    #define X 1004535809
    using namespace std;
    int n,m,tar,k,GR,a[M+5],cnt,v[M+5],LG[M+5],P[LN+5][M+5],ans[M+5];
    I int Qpow(RI x,RI y,CI p) {RI t=1;W(y) y&1&&(t=1LL*t*x%p),x=1LL*x*x%p,y>>=1;return t;}
    I bool IsP(CI x) {for(RI i=2;1LL*i*i<=x;++i) if(!(x%i)) return 0;return 1;}
    I void GR_Init()//求原根
    {
    	RI i,j;for(i=2;i^m;++i) !((m-1)%i)&&IsP(i)&&(v[++cnt]=i);
    	for(i=2;i^m;++i)//枚举
    	{
    		for(j=1;j<=cnt;++j) if(Qpow(i,(m-1)/v[j],m)%m==1) break;//验证
    		if(j>cnt) {GR=i;break;}
    	}
    	for(i=0,j=1;i^(m-1);++i,j=1LL*j*GR%m) LG[j]=i;//预处理对数
    }
    class Poly
    {
    	private:
    		int P,L,Inv,PR,IPR,R[4*M+5],A[4*M+5],B[4*M+5];
    		I int Sum(CI x,CI y) {return x+y>=X?x+y-X:x+y;}I int Sub(CI x,CI y) {return x-y<0?x-y+X:x-y;}
    		I void T(int *s,CI op)
    		{
    			RI i,j,k,U,S,x,y;for(i=0;i^P;++i) i<R[i]&&(s[i]^=s[R[i]]^=s[i]^=s[R[i]]);
    			for(i=1;i^P;i<<=1) for(U=Qpow(~op?PR:IPR,(X-1)/(i<<1),X),j=0;j^P;j+=i<<1)
    				for(S=1,k=0;k^i;++k,S=1LL*S*U%X) s[j+k]=Sum(x=s[j+k],y=1LL*S*s[i+j+k]%X),s[i+j+k]=Sub(x,y);
    		}
    	public:
    		I Poly() {PR=3,IPR=Qpow(3,X-2,X);}
    		I void Init()//因为数组大小固定,因此可以先预处理一波
    		{
    			P=1,L=0;W(P<=2*(m-2)) P<<=1,++L;Inv=Qpow(P,X-2,X);
    			for(RI i=0;i^P;++i) R[i]=(R[i>>1]>>1)|((i&1)<<L-1);
    		}
    		I void Mul(int *a,int *b,int *t)//NTT
    		{
    			RI i;for(i=0;i<=m-2;++i) A[i]=a[i],B[i]=b[i];for(;i^P;++i) A[i]=B[i]=0;
    			for(T(A,1),T(B,1),i=0;i^P;++i) A[i]=1LL*A[i]*B[i]%X;
    			for(T(A,-1),i=0;i<=m-2;++i) t[i]=1LL*(A[i]+A[i+m-1])*Inv%X;//注意把后面的加到前面
    		}
    }NTT;
    int main()
    {
    	RI i;for(scanf("%d%d%d%d",&n,&m,&tar,&k),i=1;i<=k;++i) scanf("%d",a+i);
    	for(GR_Init(),ans[0]=1,i=1;i<=k;++i) a[i]&&(P[0][LG[a[i]]]=1);//初始化第一个数组
    	for(NTT.Init(),i=1;i<=LN;++i) NTT.Mul(P[i-1],P[i-1],P[i]);//倍增
    	for(i=0;i<=LN;++i) (n>>i)&1&&(NTT.Mul(ans,P[i],ans),0);//计算答案
    	return printf("%d",ans[LG[tar]]),0;
    }
    
  • 相关阅读:
    HLS、RTSP、RTMP
    浏览器自定义协议
    CMD窗口设置代理
    Win10性能提升设置
    MongoDB
    PMP考位抢夺攻略(二)
    PMP考位抢夺攻略!
    AES php java 互转
    分解uber依赖注入库dig-源码分析
    分解uber依赖注入库dig-使用篇
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/BZOJ3992.html
Copyright © 2020-2023  润新知