把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【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\)恰好是\(1\sim 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\times c\equiv a(mod\ m)}g_{x,b}\times g_{y,c} \]

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

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

当然是用对数啦!

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

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

\[g_{x+y,w}=\sum_{u+v\equiv w(mod\ m-1)}g_{x,u}\times 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;
}
posted @ 2020-05-07 10:44  TheLostWeak  阅读(140)  评论(0)    收藏  举报