[SDOI2015]序列统计
考虑定义在取模意义下的对数函数,即如果有
\[G^k\equiv x\pmod m
\]
则 \(\log _Gx=k\).
那么,我们可以将等式变换为
\[\begin{aligned}
\prod_{i=1}^n a_i&\equiv x\pmod m \\
\Leftrightarrow\log_G\left(\prod_{i=1}^n a_i\right)&\equiv \log_Gx\pmod {\varphi(m)} \\
\Leftrightarrow\sum_{i=1}^n\log_Ga_i&\equiv \log_Gx\pmod {\varphi(m)}
\end{aligned}
\]
定义 \(f_{i,j}\) 表示选择 \(2^i\) 个数之后,\(\sum_{i=1}^n\log_Ga_i=j\) 的方案数有多少,不难得到转移
\[f_{i,j}=\sum_{k=1}^jf_{i-1,k}\times f_{i-1,j-k}
\]
这就是卷积了,最后将 \(n\) 二进制分解就可以得到答案.
而这个 \(G\) 要保证 \(\log_G\) 在取模意义下是一个函数,不难想到这个 \(G\) 必须是 \(m\) 的原根.
需要注意几个细节:
- 我们是在取 \(\log_G\) 之后取模,这个模数就不是 \(m\) 而是 \(\varphi (m)\);
- 每次做完乘法之后,要将结果数组变换回去,并且将大于等于 \(\varphi(m)\) 的部分取模,归回 \([0,\varphi(m))\) 的区间去,因为我们求和是在 \(\bmod \varphi(m)\) 的情况下进行的;
- 询问的时候不是问 \(\tt ans[x]\),而是 \(\tt ans[log(x)]\);
算法整体复杂度 \(\mathcal O(n\log m\log n)\).
代码
const int mod=1004535809;
const int g=3;
const int gi=334845270;
const int maxm=8000;
const int logn=30;
inline int qkpow(int a,int n){
int ret=1;
for(;n>0;n>>=1,a=1ll*a*a%mod)if(n&1)
ret=1ll*ret*a%mod;
return ret;
}
int f[logn+5][maxm*4+5];
int rev[maxm*4+5],len;
inline void ntt(int* f,const int n,const short opt=1){
for(int i=0;i<n;++i)if(i<rev[i])
swap(f[i],f[rev[i]]);
for(int p=2;p<=n;p<<=1){
int len=p>>1;
int w=qkpow(opt==1?g:gi,(mod-1)/p);
for(int k=0;k<n;k+=p){
int buf=1,tmp;
for(int i=k;i<k+len;++i,buf=1ll*buf*w%mod){
tmp=1ll*f[i+len]*buf%mod;
f[i+len]=(f[i]+mod-tmp)%mod;
f[i]=(f[i]+tmp)%mod;
}
}
}
if(opt==-1){
int inv=qkpow(n,mod-2);
for(int i=0;i<n;++i)f[i]=1ll*f[i]*inv%mod;
}
}
int n,m,x,S;
int s[maxm+5];
int mg,phim;
inline int get_r(const int x){
for(int i=2;i<x;++i){
for(int j=0,tmp=1;j<=x;++j,tmp=1ll*tmp*i%x){
if(tmp==1 && j>0){
if(j==x-1)return i;
break;
}
}
}
return -1;
}
inline int new_log(const int x){
if(x==0)return -1;
for(int i=0,tmp=1;i<m;++i,tmp=1ll*tmp*mg%m)
if(tmp==x)return i;
// return -1;
}
inline void input(){
n=readin(1),m=readin(1),x=readin(1),S=readin(1);
mg=get_r(m),phim=m-1;
rep(i,1,S)s[i]=new_log(readin(1));
// printf("mg == %d\n",mg);
// rep(i,1,S)printf("s[%d] == %d\n",i,s[i]);
}
inline void getf(){
for(len=1;len<=(m<<1);len<<=1);
for(int i=0;i<len;++i)
rev[i]=(rev[i>>1]>>1)|((i&1)?(len>>1):0);
rep(i,1,S)if(s[i]!=-1)++f[0][s[i]];
for(int i=1;i<=logn;++i){
ntt(f[i-1],len);// 变换之后就不需要再变回去了, 因为后面要用到的也是点积形式
for(int j=0;j<len;++j)f[i][j]=1ll*f[i-1][j]*f[i-1][j]%mod;
ntt(f[i],len,-1);
// for(int j=0;j<(m<<1);++j)
// printf("f[%d, %d] == %d\n",i,j,f[i][j]);
// 因为要进行取模运算, 所以这里将超出部分归回 [0, m) 的部分
for(int j=phim;j<len;++j){
f[i][j%phim]=(f[i][j%phim]+f[i][j])%mod;
f[i][j]=0;
}
// for(int j=0;j<len;++j)
// printf("f[%d, %d] == %d\n",i,j,f[i][j]);
// puts("---------------------------");
}
}
int ans[maxm*4+5];
inline void getans(){
ans[0]=1;
for(int i=0;i<=logn;++i)if((n>>i)&1){
ntt(ans,len);
for(int j=0;j<len;++j)
ans[j]=1ll*ans[j]*f[i][j]%mod;
ntt(ans,len,-1);
for(int j=phim;j<len;++j){
ans[j%phim]=(ans[j%phim]+ans[j])%mod;
ans[j]=0;
}
}
writc(ans[new_log(x)],'\n');
}
signed main(){
input();
getf();
getans();
return 0;
}
用到の小 \(\tt trick\)
首先,变求 \(\prod\) 为 \(\sum\),这个过程要取对数,但是这种取模意义下,可以考虑重定义对数函数,使之为在以原根为底的合法映射.
其次,对于这种求和为 \(n\) 的情况,可以考虑用类似桶的思想转化为卷积,然后使用 \(\tt NTT\) 或者 \(\tt FFT\) 解决.
例如,选 \(n\) 个数,加起来为 \(k\),我们可以先考虑设 \(t_{i,j}\) 为选 \(i\) 个数,加起来为 \(j\) 的情况,那么有
\[t_{i,j}=\sum_{k=0}^{j}t_{i-1,k}\times t_{1,j-k}
\]
然后使用卷积.
但是对于 \(n\) 比较大的情况,开不下,我们可以考虑将 \(t_{i,j}\) 由 “选 \(i\) 个数,加起来为 \(j\) 的情况” 变为 "选 \(2^i\) 个数,加起来为 \(j\) 的情况”,这就是类似二进制拆分的做法了.