[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\) 的原根.

需要注意几个细节:

  1. 我们是在取 \(\log_G\) 之后取模,这个模数就不是 \(m\) 而是 \(\varphi (m)\)
  2. 每次做完乘法之后,要将结果数组变换回去,并且将大于等于 \(\varphi(m)\) 的部分取模,归回 \([0,\varphi(m))\) 的区间去,因为我们求和是在 \(\bmod \varphi(m)\) 的情况下进行的;
  3. 询问的时候不是问 \(\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\) 的情况”,这就是类似二进制拆分的做法了.

posted @ 2021-02-02 20:23  Arextre  阅读(97)  评论(0编辑  收藏  举报