学习笔记——数学


死也学不会系列。

数学题单 1

1、质数相关

1.筛法求质数

埃筛

当我们找出一个质数时,将它在范围内的所有倍数全部标记,因为小于它的数已经处理过,所以无需标记,复杂度 \(O(n \log\log n)\)

inline void get_pr1(int n){
    memset(vis,0,sizeof(vis));
    memset(pr,0,sizeof(pr));
    for(int i=2;i<=n;i++){
        if(vis[i]) continue;
        pr[++pr[0]]=i;
        for(int j=1;i*j<=n;j++){
            vis[i*j]=1;
        }
    }
}

线性筛

埃筛的不足在于,一个质因数较多的数会被多次标记到,因此我们把标记由 \(0/1\) 改为最小的质因数,就可以避免此情况的出现。

inline void get_pr2(int n){
    memset(vis,0,sizeof(vis));
    memset(pr,0,sizeof(pr));
    for(int i=2;i<=n;i++){
        if(!vis[i]){
            vis[i]=i;
            pr[++pr[0]]=i;
        }
        for(int j=1;j<=pr[0];j++){
            if(i*pr[j]>n||pr[j]>vis[i]) break;
            vis[i*pr[j]]=pr[j];
        }
    }
}

2.唯一分解定理

对于一个正整数 \(N\),一定有:

\[N=p_1^{c_1}p_2^{c_2}\dots p_m^{c_m} \]

其中 \(p_i\) 是质数,\(c_i\) 是非负整数。

由唯一分解亦可得:

\(N\) 正约数的个数:

\[(c_1+1)\times (c_2+1)\times \dots \times(c_m+1)=\prod_{i=1}^m(c_i+1) \]

\(N\) 正约数的和:

\[(1+p_1+p_1^2+\dots+p_1^{c_1})\times \dots\times(1+p_m+p_m^2+\dots +p_m^{c_m})=\prod_{i=1}^m \sum_{j=0}^{c_i} p_i^j \]

2、欧几里得算法相关

1.欧几里得算法

用来求最大公约数和最小公倍数。

更相减损术

\(\gcd(a,b)=\gcd(a,a-b)=\gcd(b,a-b)\)

在处理大整数的最大公约问题时应用较多。

辗转相除法

\(\gcd(a,b)=\gcd(b,a\bmod b)\)

证明:

\(a,显然 \(\gcd(b,a\bmod b)=\gcd(b,a)=\gcd(a,b)\)

\(a\ge b\),设 \(a=p\times b +q\),此时 \(q=a\bmod b\),对于任意一个公约数 \(d\),都有 \(d\mid a\)\(d\mid b\),所以就有 \(d\mid (p\times b)\),即 \(d\mid (a-p\times b)\),也就是 \(d\mid q\),此情况成立。

inline int gcd(int a,int b){
    if(!b) return a;
    else return gcd(b,a%b);
}

最小公倍数

设两正整数 \(a=p_1^{k_1}p_2^{k_2}\dots p_m^{l_m},b=p_1^{l_1}p_2^{l_2}\dots p_m^{l_m}\)

由唯一分解可得:

\[\gcd(a,b)=p_1^{\min(k_1,l_1)}p_2^{\min(k_2,l_2)}\dots p_m^{\min(k_m,l_m)} \]

\[\operatorname{lcm}(a,b)=p_1^{\max(k_1,l_1)}p_2^{\max(k_2,l_2)}\dots p_m^{\max(k_m,l_m)} \]

\[a\times b=p_1^{k_1+l_1}p_2^{k_2+l_2}\dots p_m^{k_m+l_m} \]

因此有:

\[\operatorname{lcm}(a,b)=\dfrac{a\times b}{\gcd(a,b)} \]

多个数求gcd以及lcm

\[\gcd(a,b,c)=\gcd(\gcd(a,b),c) \]

\[\operatorname{lcm}(a,b,c)=\operatorname{lcm}(\operatorname{lcm}(a,b),c) \]

\(\operatorname{lcm}\) 时,先求出前两个数的 \(\operatorname{lcm}\),再与下一个数一起求 \(\operatorname{lcm}\),以此类推。

2.扩展欧几里得算法

用于求 \(ax+by=\gcd(a,b)\) 解。

求一组可行解

由欧几里得算法得,\(\gcd(a,b)=\gcd(b,a\bmod b)\)

所以列含参方程得:

\[\begin{cases} ax_1+by_1=\gcd(a,b)\\bx_2+(a\bmod b)y_2=\gcd(b,a \bmod b)\end{cases} \]

于是有:

\[ax_1+by_1=bx_2+(a\bmod b)y_2 \]

\[ax_1+by_1=bx_2+(a-\lfloor \dfrac{a}{b} \rfloor\times b)y_2 \]

\[ax_1+by_1=ay_2+b(x_2-\lfloor \dfrac{a}{b} \rfloor\ y_2) \]

所以:\(x_1=y_2,y_1=(x_2- \lfloor \frac{a}{b}\rfloor y_2)\),不断递归求解可以得到。

inline int exgcd(int a,int b,int &x,int &y){
    if(!b){
        x=1,y=0;
        return a;
    }
    int d=exgcd(b,a%b,x,y);
    int t=x;
    x=y,y=t-a/b*y;
    return d;
}

对于上述的方程是一定有解的,并且存在一组 \((x,y)\) 满足 \(|x|\le b,|y| \le a\),我们称其为裴蜀定理

求任意一组通解

设已经求出的可行解为 \((x_0,y_0)\),带入到方程中有:

\[\begin{cases} ax_0+by_0=\gcd(a,b)\\ax+by=\gcd(a,b)\end{cases} \]

联立并整理得:

\[a(x_0-x)=b(y-y_0) \]

\[\dfrac{a}{\gcd(a,b)}(x_0-x)=\dfrac{b}{\gcd(a,b)}(y-y_0) \]

容易发现这个等式左右两边的前一个因式是互质的,因此后一个因式与前一个因式是对应整除关系(根据唯一分解定理可以得到),设这个商为 \(k\),就有:

\[\begin{cases}x_0-x =\dfrac{b}{\gcd(a,b)}\times k\\\\y-y_0=\dfrac{a}{\gcd(a,b)}\times k\end{cases} \]

整理得到:\(x=x_0-\frac{b}{\gcd(a,b)}\times k,y=y_0+\frac{a}{\gcd(a,b)}\times k\),其中 \(k\in \mathbb{Z}\)

推广到不定方程

对于不定方程 \(ax+by=c\),解决方式如下:

求解 \(ax+by=\gcd(a,b)\) 的一组解 \((x_0,y_0)\),同乘 \(\frac{c}{\gcd(a,b)}\),就能得到不定方程的一组可行解 \((x,y)=(x_0\times\frac{c}{\gcd(a,b)},y_0\times \frac{c}{\gcd(a,b)})\),而我们把它代入到通解的式子中,得到最终的结果:

\[\begin{cases} x=\dfrac{c}{\gcd(a,b)}\times x_0- \dfrac{b}{\gcd(a,b)}\times k \\\\ y=\dfrac{c}{\gcd(a,b)}\times y_0+ \dfrac{a}{\gcd(a,b)}\times k\end{cases} \]

特别地,\(x\) 的最小整数解为 \((x \bmod \frac{b}{\gcd(a,b)}+\frac{b}{\gcd(a,b)})\bmod\frac{b}{\gcd(a,b)}\)

例题

1.P4549 裴蜀定理

其实就是要求一个 \(a_1x_1+a_2x_2+\dots+a_nx_n\) 的最小值,发现前两项和的最小值为 \(\gcd(a_1,a_2)\),因此一边读入一边求 \(\gcd\) 即可。

inline ll gcd(ll a,ll b){
    if(!b) return a;
    else return gcd(b,a%b);
}
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        int a=read();
        ans=gcd(ans,llabs(a));
    }
    printf("%lld\n",ans);
    return 0;
}

2.P5656 二元一次方程(exgcd)

需要推式子……

\(a'=\frac{a}{\gcd(a,b)},b'=\frac{b}{\gcd(a,b)}\),为保证 \(x \ge 1\),需要有 \(x_0-b'k\ge1\),得到:

\[k\ge \lceil\frac{1-x_0}{b'}\rceil,x_{min}=x_0+\lceil\frac{1-x_0}{b'}\rceil\times b' \]

其对应的 \(k\) 可以得到 \(y_{max}\),而\(y_{min}=y_{max}\bmod a'\),对应可以得到 \(x_{max}\),总之就是很“模板”。

3.求解同余方程

方程 \(ax+by=c\) 与同余方程 \(ax \equiv c\pmod b\) 是等价的,因此求解只需要按照上面的方法就可以了。

例题

1.P1082 同余方程

板子……

inline int exgcd(int a,int b,int &x,int &y){
    if(!b){
        x=1,y=0;
        return a;
    }
    int d=exgcd(b,a%b,x,y);
    int t=x;
    x=y,y=t-a/b*y;
    return d;
}
int main(){
    int a=read(),b=read();
    int x=0,y=0;
    int d=exgcd(a,b,x,y);
    x=(x+b)%b;
    printf("%d\n",x);
    return 0;
}

2.P1516 青蛙的约会

因为是首尾相连的路径所以题目是在求一个:

\[pos_1+sp_1\times k \equiv pos_2+sp_2\times k\pmod l \]

我们整理一下,可以得到:

\[(sp_1-sp_2)\times k-l\times q=(pos_2-pos_1) \]

其中 \(q\in \mathbb{Z}\),设 \(a=sp_1-sp_2,b=l,c=pos_2-pos_1\),就得到了方程:

\[ak+bq=c \]

\(k\) 的最小整数解即可。

inline ll exgcd(ll a,ll b,ll &x,ll &y){
    if(!b){
        x=1,y=0;
        return a;
    }
    ll d=exgcd(b,a%b,x,y);
    ll t=x;
    x=y,y=t-a/b*y;
    return d;
}
ll pos1,pos2,sp1,sp2,l;
int main(){
    pos1=read(),pos2=read(),sp1=read(),sp2=read(),l=read();
    ll a=sp1-sp2,b=l,c=pos2-pos1,x=0,y=0;
    if(a<0) a=-a,c=-c;
    ll d=exgcd(a,b,x,y);
    if(c%d){
        printf("Impossible\n");
    }
    else{
        printf("%lld\n",((x*c/d)%(b/d)+(b/d))%(b/d));
    }
    return 0;
}

3.P1292 倒酒

4.乘法逆元

如果有 \(ax\equiv 1\pmod b\),就称 \(x\)\(a\bmod \ b\) 的逆元,记作 \(a^{-1}\)

扩展欧几里得求逆元

因为 \(ax\equiv 1\pmod b\) 等价于 \(ax+by=1\),因此可以解这个不定方程,来得到逆元的值。

费马小定理

\(p\) 为质数,且 \(\gcd(a,p)=1\),则有:\(a^{p-1}\equiv 1\pmod p\),也可写作:\(a^p\equiv a\pmod p\)

快速幂求逆元

由费马小定理可得:

\[ax\equiv 1\equiv a^{b-1} \pmod b \]

此时,\(b\) 是质数,所以可以得到:

\[x \equiv a^{b-2}\ \pmod b \]

用快速幂可以 \(O(logn)\) 求出一个数的逆元。

线性求 \([1,n]\) 的逆元

因为 \(1\) 在任何模定义下的逆元都为 \(1\),所以可以根据这个性质递推得解。

已知同余方程 \(i\times x\equiv 1\pmod p\),其中 \(x\) 的值为 \(\bmod \ p\)\(i^{-1}\) 的值。

\(k=\lfloor\frac{p}{i}\rfloor,j=p \bmod i,p=ki+j\),所以就有同余方程 \(ki+j\equiv0 \pmod p\)

方程两边同乘 \(i^{-1}\times j^{-1}\) 并移项可得:\(i^{-1}\equiv-kj^{-1} \pmod p\)

\(k=\lfloor\frac{p}{i}\rfloor,j=p \bmod i\) 代入得到:\(i^{-1}\equiv-\lfloor\frac{p}{i}\rfloor\times (p\bmod i)^{-1} \pmod p\)

因为 \(p\bmod i\) 的逆元是已知的,直接代入求解即可。

求任意 \(n\) 个的逆元

我们先用求出 \(pord(i)\),表示前 \(i\) 个数的前缀积,用快速幂求出 \(prod(n)\) 的逆元,设为 \(pinv(n)\),也就是 \(pinv(n)= prod(n)^{b-2}\),接下来对于每一个 \(i\ (i\in[1,n-1])\)\(pinv(i)=pinv(i+1)\times a(i+1)\),而每个数的逆元就是 \(inv(i)=pinv(i)\times prod(i-1)\)

ll n,b;
ll a[maxn],prod[maxn],prod_inv[maxn],inv[maxn];
inline ll q_pow(ll x,ll p){
    ll ans=1;
    x=(x%b+b)%b;
    while(p){
        if(p&1){
            ans=ans*x%b;
        }
        x=x*x%b;
        p>>=1;
    }
    return ans;
}
int main(){
    n=read(),b=read();
    prod[0]=1;
    for(int i=1;i<=n;i++){
        a[i]=read();
        prod[i]=prod[i-1]*a[i]%b;
    }
    prod_inv[n]=q_pow(prod[n],b-2);
    for(int i=n-1;i>=1;i--){
        prod_inv[i]=prod_inv[i+1]*a[i+1]%b;
    }
    for(int i=1;i<=n;i++){
        inv[i]=prod[i-1]*prod_inv[i]%b;
        printf("%lld\n",inv[i]);
    }
    return 0;
}

例题

1.P3811 乘法逆元

模板题,代码同上。

2.P5431 乘法逆元2

同上上,需要注意一下卡常。

3.P2054 AHOI2005 洗牌

我们通过找规律可以发现形如 \({1,2,3,4,5,6,7,8,9,10}\) 的数列在经过一次洗牌后变作 \({6,1,7,2,8,3,9,4,10,5}\),也就是对于洗牌前的位置 \(i\),洗牌后的 \(i'\) 为:

\[i'=\begin{cases} i\times 2 \ (i\le\dfrac{n}{2})\\i\times 2-(n+1) \ (i>\dfrac{n}{2})\end{cases} \]

也就有:

\[i'=(i\times 2)\bmod (n+1) \]

那么根据题意求 \(n\) 纸牌在进行 \(m\) 次变换后第 \(l\) 张的初始位置(初始位置和其数值是等价的),设这个初始位置为 \(x\),根据式子可以得到:

\[x\times 2^m \equiv\ l\pmod {n+1} \]

为了得到 \(x\),我们需要在方程两边乘上 \(2\)\(\bmod \ n+1\) 定义下的逆元 \(inv\),来约去 \(2^m\),因此就是:

\[x\times 2^m\times inv^m\equiv x\equiv l\times inv^m\pmod {n+1} \]

求这个式子即可,注意这里不保证模数 \(n+1\) 是质数,因此不能用快速幂求逆元,同时需要使用快速幂与龟速乘结合。

inline ll exgcd(ll a,ll b,ll &x,ll &y){
    if(!b){
        x=1,y=0;
        return a;
    }
    ll d=exgcd(b,a%b,x,y);
    ll t=x;
    x=y,y=t-a/b*y;
    return d;
}
inline ll s_mul(ll x,ll p){
    ll ans=0;
    while(p){
        if(p&1){
            ans=(ans+x)%(n+1);
        }
        x=(x+x)%(n+1);
        p>>=1;
    }
    return ans%(n+1);
}
inline ll q_pow(ll x,ll p){
    ll ans=1;
    while(p){
        if(p&1){
            ans=s_mul(ans,x);
        }
        x=s_mul(x,x);
        p>>=1;
    }
    return ans%(n+1);
}

ll inv,ans,tmp;
int main(){
    n=read(),m=read(),l=read();
    exgcd(2,n+1,inv,tmp);
    inv=(inv%(n+1)+n+1)%(n+1);
    inv=q_pow(inv,m);
    ans=s_mul(l,inv);
    printf("%lld\n",ans);
    return 0;
}

4.P2613 有理数取余

\(\dfrac{a}{b} \bmod 19260817\),并不需要大整数,因为是逆元的问题,在快读时一边读入一边取模就可以。

inline ll mread(){
    ll x=0,w=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
    while(c<='9'&&c>='0'){x=x*10%mod+(c-'0')%mod;c=getchar();}
    return x%mod*w;
}

3、欧拉定理相关

1.欧拉函数

\(\varphi(N)\) 表示小于等于 \(N\) 且与 \(N\) 互质的正整数个数,即

\[\varphi(N)=\sum_{i=1}^{N}\ [\gcd(i,N)=1] \]

特别地,\(\varphi(p)=p-1\)\(p\) 为质数。

由容斥原理可以得到:

\[\varphi(N)=N\times \dfrac{p_1-1}{p_1}\times \dfrac{p_2-1}{p_2} \times \dots \times \dfrac{p_m-1}{p_m}=N\times \prod_{p\mid N}(1-\dfrac{1}{p}) \]

inline int euler1(int n){
    int ans=n;
    for(int i=2;i<=sqrt(n);i++){
        if(n%i==0){
            ans=ans/i*(i-1);
            while(n%i==0){
                n/=i;
            }
        }
    }
    if(n>1){
        ans=ans/n*(n-1);
    }
    return ans;
}

欧拉函数具有积性函数的性质。

\(\gcd(a,b)=1\),则 \(f(a)\times f(b)=f(a\times b)\),则 \(f\) 是积性函数,而对于 \(\varphi(a)\times \varphi(b)\) 而言,因为 \(a\)\(b\) 互质,带入到求欧拉函数的公式当中后,每一项 \(\frac{p-1}{p}\) 都不重复,所以性质成立。

\(n\) 个数的欧拉函数

int phi[maxn];
inline void get_euler1(int n){
    for(int i=1;i<=n;i++){
        phi[i]=i;
    }
    for(int i=2;i<=n;i++){
        if(phi[i]==i){
            for(int j=1;i*j<=n;j++){
                phi[i*j]=phi[i*j]/i*(i-1);
            }
        }
    }
}

例题

1.P2158 SDOI2008 仪仗队

不难发现,当且仅当 \(\gcd(x,y)=1\) 时,\((x,y)\) 能够被看到,因此答案应为 \(1+2\times \sum_{i=1}^{n-1} \varphi(i)\),筛出欧拉函数值即可。

int phi[maxn];
inline void get_euler(int n){
    for(int i=1;i<=n;i++){
        phi[i]=i;
    }
    for(int i=2;i<=n;i++){
        if(phi[i]==i){
            for(int j=1;i*j<=n;j++){
                phi[i*j]=phi[i*j]/i*(i-1);
            }
        }
    }
}
int ans;
int main(){
    n=read()-1;
    if(!n){
        printf("0\n");
        return 0;
    }
    get_euler(n);
    for(int i=1;i<=n;i++){
        ans+=phi[i];
    }
    ans=ans*2+1;
    printf("%d\n",ans);
    return 0;
}

2.P2303 SDOI2012 Longge的问题

\[\begin{aligned} \sum_{i=1}^n\gcd(i,n)&= \sum_{d\mid n}d\cdot \sum_{i=1}^n[\gcd(i,n)=d]\\&= \sum_{d\mid n}d\cdot \sum_{i=1}^{n/d}\left[\gcd\left(i,\dfrac nd\right)=1\right]\\&= \sum_{d\mid n}d\cdot\varphi\left(\dfrac nd\right) \end{aligned}\]

3.P2155 SDOI2008 萨拉公主的困惑

要求 \([1,n!]\) 中整数与 \(m!\ (m\le n)\) 互质的个数。

先考虑 \([1,m!]\) 的部分:

\[\begin{aligned} \sum_{i=1}^{m!}[\gcd(i,m!)=1]=\varphi(m!) \end{aligned}\]

由欧几里得算法得:

\[\gcd(a,b)=\gcd(a\bmod b,b) \]

所以有:

\[\gcd(m!\times k+i,m!)=\gcd(i,m!) \]

因此 \([m!\times (k-1)+1,m!\times k]\ (1\le k \le \frac{n!}{m!})\)\([1,m!]\) 的部分都是等价的,所以有:

\[\begin{aligned} \sum_{i=1}^{n!} [\gcd(i,m!)=1]&=\dfrac{n!}{m!}\times \sum_{i=1}^{m!} [\gcd(i,m!)=1]\\&=\dfrac{n!}{m!}\times \varphi(m!) \end{aligned}\]

现在考虑 \(\varphi(m!)\) 怎么求,已知 \(\varphi(i)\) 的递推式:

\[\varphi(i)=i\times \prod_{p\mid i}\dfrac{p-1}{p} \]

所以可以得到:

\[\varphi(i!)=\begin{cases} i\times (i-1)!\times \prod_{p\mid i!} \left(\frac{p-1}{p}\right)\times \frac{i-1}{i} &(i\in \mathbb{P})\\\\ i\times (i-1)!\times \prod_{p\mid i!}\left(\frac{p-1}{p}\right) &(i\not\in\mathbb{P}) \end{cases}\]

也就是说 \(i\) 是否为素数只会影响到求积中是否会添加一项,化简就有:

\[\varphi(i!)=\begin{cases} \varphi((i-1)!)\times (i-1) &(i\in \mathbb{P})\\\\ \varphi((i-1)!)\times i &(i\not\in \mathbb{P}) \end{cases}\]

需要注意的是题目中的范围没有明确给出 \(R>n\),所以可能存在 \(R\mid n!\),使逆元不存在,因此需要处理时模去 \(R\),并且特判是否答案为 \(0\)

inline void get_pr(){
    vis[0]=vis[1]=1;
    for(int i=2;i<=base;i++){
        if(!vis[i]){
            pr[++pr[0]]=i;
        }
        for(int j=1;j<=pr[0];j++){
            if(i*1ll*pr[j]>base) break;
            vis[i*pr[j]]=1;
            if(i%pr[j]==0) break;
        }
    }
}
inline ll q_pow(int x,int p){
    ll ans=1;
    while(p){
        if(p&1){
            ans=ans*1ll*x%r;
        }
        x=x*1ll*x%r;
        p>>=1;
    }
    return ans;
}
int main(){
    t=read(),r=read();
    get_pr();
    fac[0]=1,fphi[0]=1;
    for(int i=1;i<=base;i++){
        int tmp=i;
        while(tmp%r==0){
            tmp/=r;
        }
        fac[i]=fac[i-1]*1ll*tmp%r;
        tmp=vis[i]?i:i-1;
        while(tmp%r==0){
            tmp/=r;
        }
        fphi[i]=fphi[i-1]*1ll*tmp%r;
    }
    while(t--){
        n=read(),m=read();
        if(n/r>m/r){
            printf("0\n");
            continue;
        }
        ll ans=fac[n]*1ll*fphi[m]%r*q_pow(fac[m],r-2)%r;
        printf("%lld\n",ans);
    }
    return 0;
}

4、组合数学

\[\mathrm{C}_n^m=\dfrac{n!}{m!(n-m)!}\quad \mathrm{A}_n^m=\dfrac{n!}{(n-m)!} \]

卢卡斯定理

用于解决 \(n,m\) 值较大的排列数,要求模数 \(p\in\mathbb{P}\)

\[\dbinom{n}{m}\bmod p=\dbinom{\lfloor n/p\rfloor}{\lfloor m/p\rfloor}\cdot\dbinom{n\bmod p}{m\bmod p}\bmod p \]

inline ll C(int n,int m){
    if(n==0||m==0) return 1;
    return fac[n]*finv[m]%mod*finv[n-m]%mod;
}
inline ll Lucas(int n,int m){
    if(!m) return 1;
    return (C(n%mod,m%mod)*Lucas(n/mod,m/mod))%mod;
}

二项式定理

二项式系数公式:

\[(a+b)^n=\sum_{i=0}^n \dbinom{n}{i} a^{n-i}b^i \]

错排列

已知 \(n\) 个数和 \(n\) 个位置,且每个位置有一个对应的数不能放置,求排列情况数。

设情况数 \(D_n\),模拟可以得到 \(D_1=0,D_2=1,D_3=2\),考虑递推,要求一步得到符合条件的序列,若前 \(n-1\) 个是符合条件的序列,则与任意一个调换都可以,因此有 \((n-1)\times D_{n-1}\);若有且只有一个不符合条件,则需要和它调换位置,因此有 \((n-1)\times D_{n-2}\),综上递推式为 \(D_n=(n-1)\times (D_{n-1}+D_{n-2})\)

圆排列

在排列的基础上连链成环,因此要除去重复情况(显然 \({1,3,2,4}\)\({2,3,4,1}\) 在首尾相连成环以后是等价的),因此圆排列数为 \(A_n^n/n=(n-1)!\)

例题

1.P3233 HNOI2012 排队

分情况讨论,是由男生隔开老师还是由女生隔开老师。

若男生隔开老师,则情况数为男生人数的排列 \(n!\) 乘上老师可选择的位置与个数的排列 \(A_{n+1}^2\),以及女生可选择位置与个数的排列 \(A_{n+3}^m\)

若女生隔开老师,则情况数为男生人数的排列 \(n!\) 乘上隔开老师的女生情况数 \(m\) 乘上老师个数的排列 \(2!\),将老师与这名女生视为一个整体,可以放的位置有 \((n+1)\) 个,剩余女生可选择的位置与个数的排列为 \(A_{n+2}^{m-1}\)

所以最终结果:

\[\operatorname{ans}=n!\times \mathrm{A}_{n+1}^2\times \mathrm{A}_{n+3}^m + n!\times m\times 2\times (n+1)\times \mathrm{A}_{n+2}^{m-1} \]

化简用大整数求解。

2.P4701 SDOI2016 排列计数

错排列的板子,答案为 \(\mathrm{C}_n^m\times D_{n-m}\)

int main(){
    t=read();
    fac[0]=finv[0]=1;
    for(int i=1;i<=maxn;i++){
        fac[i]=fac[i-1]*i%mod;
    }
    finv[maxn]=q_pow(fac[maxn],mod-2);
    for(int i=maxn-1;i>=1;i--){
        finv[i]=finv[i+1]*(i+1)%mod;
    }
    d[1]=0,d[2]=1,d[3]=2;
    for(int i=4;i<=maxn;i++){
        d[i]=(i-1)*(d[i-2]+d[i-1])%mod;
    }
    while(t--){
        n=read(),m=read();
        if(m+1==n){
            printf("0\n");
            continue;
        }
        if(m==n){
            printf("1\n");
            continue;
        }
        if(m==0){
            printf("%lld\n",d[n]);
            continue;
        }
        ll ans=fac[n]%mod*finv[m]%mod*finv[n-m]%mod*d[n-m]%mod;
        printf("%lld\n",ans);
    }
    return 0;
}

3.P2606 ZJOI2010 排列计数

发现满足条件的数列满足一个小根堆的性质,我们将数列扩大一倍,在原有的叶子节点下加上一层。

设以 \(i\) 为根的子堆有 \(dp(i)\) 种符合条件的情况,设 \(siz(i)\) 表示以 \(i\) 为根子堆的大小,那么就有:

\[dp(i)=dp(i\times 2)\times dp(i\times 2+1)\times \dbinom{siz(i)-1}{siz(i\times 2)} \]

因为小根堆与二叉搜索树有明显的区别,因此只要是子树中的节点无论怎样排列都满足性质,我们不需要考虑它具体的分布。

inline ll q_pow(ll x,ll p){
    ll ans=1;
    while(p){
        if(p&1){
            ans=ans*x%mod;
        }
        x=x*x%mod;
        p>>=1;   
    }
    return ans;
}
inline ll C(ll n,ll m){
    return fac[n]*q_pow(fac[m]*fac[n-m]%mod,mod-2)%mod;
}
inline ll Lucas(ll n,ll m){
    if(!m) return 1;
    return (C(n%mod,m%mod)*Lucas(n/mod,m/mod))%mod;
}
int main(){
    n=read(),mod=read();
    fac[0]=1;
    for(int i=1;i<=n;i++){
        fac[i]=fac[i-1]*i%mod;
        siz[i]=1;
    }
    for(int i=n;i>=1;i--){
        siz[i>>1]+=siz[i];
    }
    for(int i=n+1;i<=2*n+1;i++){
        dp[i]=1;
    }
    for(int i=n;i>=1;i--){
        dp[i]=dp[i<<1]*dp[i<<1|1]%mod*Lucas(siz[i]-1,siz[i<<1])%mod;
    }
    printf("%lld\n",dp[1]);
    return 0;
}

4.集合选数

题意:设一个集合 \(S\)\(n\) 个元素,可以得到它有 \(2^n\) 个子集(包括 \(\varnothing\)),现在从中选出任意多个子集(至少一个),使他们交集的元素为 \(k\) ,求方案数,答案对 \(1e9+7\) 取模。

首先选 \(k\) 个数的方案数为 \(\mathrm{C}_n^k\),而答案就应该为选数的方案数乘 \(n-k\) 个元素没有交集的方案数,显然会用到到容斥原理。

\(f(i)\) 表示交集大于等于 \(i\) 的元素个数,根据上面可以得到方案数也应为 \(\mathrm{C}_n^i\) 乘上一个选择方案数,那么现在考虑这个选择方案数。

可以得到剩下的 \(n-i\) 个数组成的集合 \(S'\) 的子集个数为 \(2^{n-i}\),那么我们选择这些子集的方案数为 \(2^{2^{n-i}}\),注意到我们是不能选空集的,方案数应当为 \(2^{2^{n-i}}-1\),综上 \(f(i)=\mathrm{C}_n^i\times 2^{2^{n-1}}\)

接下来考虑容斥,应当为“一加一减”,注意 \(f(i)\)\(i\in[0,n-k]\),所以是加上偶数项减去奇数项。

实现的话,费马小定理线性求逆元以及欧拉定理降幂即可,注意 \(f(i)\) 中的 \(n\) 实际是排除已经选择的 \(k\) 个元素后的 \(n-k\)

ll n,k;
ll fac[maxn],inv[maxn];
ll f[maxn];
inline ll q_pow(ll x,ll p,ll md){
    ll ans=1;
    x=x%md;
    while(p){
        if(p&1){
            ans=ans*x%md;
        }
        x=x*x%md;
        p>>=1;
    }
    return ans;
}
inline ll C(ll n,ll m){
    return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
inline ll Lucas(ll n,ll m){
    if(!m) return 1;
    return C(n/mod,m/mod)*Lucas(n%mod,m%mod)%mod;
}
ll ans,sum;
int main(){
    n=read(),k=read();
    fac[0]=inv[0]=1;
    for(int i=1;i<=n;i++){
        fac[i]=fac[i-1]*i%mod;
    }
    inv[n]=q_pow(fac[n],mod-2,mod);
    for(int i=n-1;i>=1;i--){
        inv[i]=inv[i+1]*(i+1)%mod;
    }
    for(int i=0;i<=n-k;i++){
        ll power=q_pow(2,n-k-i,mod-1);
        f[i]=(C(n-k,i)*(q_pow(2,power,mod)-1))%mod;
        if(i&1) f[i]=-f[i];
        sum=(sum+f[i]+mod)%mod;
    }
    ans=(C(n,k)*sum)%mod;
    ans=(ans+mod)%mod;
    printf("%lld\n",ans);
    return 0;
}

5.P6191 Bulls And Cows S

求有 \(i\) 头公牛时,除去母牛后剩下位置与公牛数的组合数,i的边界需要分类讨论。

假设 \(1\) 头公牛搭配 \(k\) 头奶牛,每组就是 \((k+1)\) 头牛,若 \((k+1)\mid n\),公牛最多有 \(k/(n+1)\) 头,而如果 \((k+1)\nmid n\),剩余的位置还可以放 \(1\) 头公牛,因此公牛最多有 \(k/(n+1)+1\) 头。

这里为了简便完成分类讨论,我们可以设置循环范围 \([0,(n-1)/(k+1)+1]\),可以发现因为设置一定会 \(+1\),所以当且仅当出现整除情况时与实际情况不符,因此把数量减小,不整除的情况数量不会改变,整除的情况就会与实际情况相符。(分块常用求块的个数)

\[\operatorname{ans}=\sum_{i=0}^{(n-1)/(k+1)+1}\ \dbinom{n-(i-1)\times k}{i} \]

PS:考虑到即便整除,在不排满的情况下,也可以选择将 \((i-1)\times k\) 头奶牛夹在 \(i\) 头公牛中间,所以组合数应选择 \(n-(i-1)\times k\)

int main(){
    n=read(),k=read();
    fac[0]=inv[0]=1;
    for(int i=1;i<=n;i++){
        fac[i]=fac[i-1]*i%mod;
    }
    inv[n]=q_pow(fac[n],mod-2);
    for(int i=n-1;i>=1;i--){
        inv[i]=inv[i+1]*(i+1)%mod;
    }
    for(int i=0;i<=(n-1)/(k+1)+1;i++){
        ans=(ans+Lucas(n-(i-1)*k,i))%mod;
    }
    printf("%lld\n",ans);
    return 0;
}

6.序列统计

题意:给定三个整数 \(n\),\(\ l\),\(\ r\) 求出长度从 \(1\)\(n\) 的,元素均在 \([l,r]\) 内的单调不下降序列数,答案对 \(1e6+3\) 取模。

借本题详细的说一说此类问题。

首先来看一个化简的题面:求长度为 \(n\),元素均在 \([1,m]\) 内的单调不下降序列个数。其实所为“单调不下降”是无需要考虑的,我们假设把这 \(m\) 个数字对应箱子从小到大依次排列,将 \(n\) 个球放入其中,所得到的的序列一定是“单调不下降”的。

那设第 \(i\) 个箱子放的球的个数为 \(x_i\),可以得到一个线性不定方程:

\[n=\sum_{i=1}^{m} x_i \]

这个方程的正整数解有多少种呢?用隔板法得到答案为 \(\binom{n-1}{m-1}\),而因为有的数可以不选取,所以真正所求是方程的非负整数解,不妨设 \(y_i=x_i+1\),方程可化为:

\[m+n=\sum_{i=1}^{m} y_i \]

这里的 \(x_i\) 就可以作为 \(0\) 存在了,同理方案数为 \(\binom{m+n-1}{m-1}\)

这样一来,我们的最终答案为 \(\sum_{i=1}^{n} \binom{m+i-1}{m-1}\)

根据杨辉三角可以得到 \(\binom{x}{y}=\binom{x-1}{y}+\binom{x-1}{y-1}\)

在原式基础上添加一个 \(\binom{m}{m}\),就能得到:

\[\begin{aligned} \sum_{i=1}^n \dbinom{m+i-1}{m-1}+\dbinom{m}{m}&= \dbinom{m}{m}+\dbinom{m}{m-1}+\dbinom{m+1}{m-1}+\cdots+\dbinom{m+n-2}{m-1}+\dbinom{m+n-1}{m-1}\\&= \dbinom{m+1}{m}+\dbinom{m+1}{m-1}+\cdots+\dbinom{m+n-2}{m-1}+\dbinom{m+n-1}{m-1}\\&= \dbinom{m+n-1}{m}+\dbinom{m+n-1}{m-1}\\&= \dbinom{m+n}{m} \end{aligned}\]

因此最终答案:

\[\operatorname{ans}=\dbinom{m+n}{m}-1 \]

7.P2154 SDOI2009 虔诚的墓主人

部分分

暴力前缀和+组合数

\[\operatorname{ans}=\sum_{i=1}^n\sum_{j=1}^m \dbinom{u}{k}\cdot\dbinom{d}{k}\cdot\dbinom{l}{k}\cdot\dbinom{r}{k}\ (u,d,l,r\ge k) \]

正解

首先因为地图很大而数量很少,所以考虑离散化,同时我们发现在同一横坐标的两棵树之间,其上下树的数量是不变的,只需要考虑左右树的数量,这个乘积需要支持查询和修改,考虑树状数组。

我们只需要预处理出组合数的值再判断并运算即可。

int n,m,w,k;
int x[maxn],y[maxn];
int fac[maxn][15],cnt[maxn][2];
pair p[maxn];
struct BIT{
    int num[maxn];
    inline int lowbit(int x){
        return x&(-x);
    }
    inline void update(int x,int k){
        k%=mod;
        while(x<=w){
            num[x]=(num[x]+k)%mod;
            x+=lowbit(x);
        }
    }
    inline int query(int x){
        int res=0;
        while(x){
            res=(res+num[x])%mod;
            x-=lowbit(x);
        }
        return res;
    }
}tree;
int ans;
int cntw[maxn];
int main(){
    n=read(),m=read(),w=read();
    for(int i=1;i<=w;i++){
        p[i].first=x[i]=read(),p[i].second=y[i]=read();
    }
    k=read();
    fac[0][0]=1;
    for(int i=1;i<=w;i++){
        fac[i][0]=1;
        for(int j=1;j<=k;j++){
            fac[i][j]=(fac[i-1][j-1]+fac[i-1][j])%mod;//预处理出(0,0)到(w,k)的组合数
        }
    }
    sort(x+1,x+w+1);
    sort(y+1,y+w+1);
    for(int i=1;i<=w;i++){
        p[i].first=lower_bound(x+1,x+w+1,p[i].first)-x;
        p[i].second=lower_bound(y+1,y+w+1,p[i].second)-y;
        cnt[p[i].first][0]++,cnt[p[i].second][1]++;//统计前缀和并离散化
    }
    //横坐标为第一关键字,纵坐标为第二关键字排序
    sort(p+1,p+w+1);
    int cntn=0,cnts=1;
    for(int i=1;i

8.P2467 SDOI2010 地精部落

话说这是孟周师兄暑假讲的一道题。他称其为“排列dp”,状态与排名有关。

首先可以发现,上升与下降实际上是等价的,因为将每个数用 \(n+1\) 减去后既不重复又恰好是与原数列相反的。

考虑dp,\(dp(i,j)\)表示前 \(i\) 个数中第 \(i\) 个数在该数列的排名(从小到大)为 \(j\),我们只考虑开头为山峰的数列,那么分两种情况:

  • \(i\) 为奇数时,\(h_i>h_{i-1}\),所以 \(dp(i,j)\) 可以由在前 \(i-1\) 个数中排名为 \([1,j-1]\) 的转移而来,注意到这里取 \(j\) 是不可行的,因为放入一个大于 \(h_{i-1}\) 的数,\(h_{i-1}\) 的排名不会改变。
  • \(i\) 为偶数时,\(h_i,所以 \(dp(i,j)\) 可以由在前 \(i-1\)个数中排名为 \([j,i-1]\) 的的转移而来,注意到这里取 \(j\) 是可行的,因为放入一个小于 \(h_{i-1}\) 的数,\(h_{i-1}\) 的排名会增加一位。

初始状态为 \(dp(i,j)=1\),接着得到递推式:

\[dp(i,j)=\begin{cases}\sum_{k=1}^{j-1} dp(i-1,k)&j=2\times z+1\\\\ \sum_{k=j}^{i-1} dp(i-1,k)&j=2\times z +2 \end{cases}\]

其中 \(z\in \mathbb{N}\)(只是为了表示奇偶数)

发现单次转移是 \(O(n)\) 的,状态数是 \(O(n^2)\) 的,显然不过,考虑用前后缀和来优化(根据转移方程)

int main(){
    n=read(),p=read();
    dp[1][1]=1;
    for(int i=2;i<=n;i++){
        sum=0;
        if(i&1){
            dp[i][1]=0;
            for(int j=2;j<=i;j++){
                sum=(sum+dp[i-1][j-1])%p;
                dp[i][j]=sum;
            }
        }
        else{
            dp[i][i]=0;
            for(int j=i-1;j>=1;j--){
                sum=(sum+dp[i-1][j])%p;
                dp[i][j]=sum;
            }
        }
    }
    for(int i=1;i<=n;i++){
        ans=(ans+dp[n][i])%p;
    }
    ans=(ans*2)%p;
    printf("%d\n",ans);
    return 0; 
}

9.P3330 ZJOI2011 看电影

10.P3214 HNOI2011 卡农

先不考虑“排列”,“重复”的问题,设 \(dp(i)\) 表示 \(i\) 个数的合法方案数。

由题可得对一个合法的集合有如下三个要求:

  • 集合中不含表示空集的元素
  • 集合中不含重复的元素
  • 集合中所有元素内的元素之和应都为偶数

若不考虑三者,可以发现,我们第 \(i\) 个集合可视为前 \(i-1\) 个的补充,也就是哪些元素目前还是奇数个,就在其中添加,因此有 \(f(i)=\mathrm{A}_{2^n-1}^{i-1}\)

接着考虑满足条件与否,因为不能包含空集,也就是说前 \(i-1\) 个集合中必定包含奇数个的元素,否则是不合法的,而这个不合法的集合显然对于 \(i-1\) 个集合是满足要求的,所以这里的不合法情况数恰恰为 \(f(i-1)\)。接着若有重复元素又该如何去掉,考虑乘法原理,不合法情况数=与第 \(i\) 个集合重复集合的位置情况数 \(\times\) 重复集合的情况数 \(\times\) 去重后的情况数。重复位置的情况数自然是 \(i-1\),因为目前只有一组集合重复,所以重复的集合种类要排除其余 \(i-2\) 个,也就是 \(2^n-1-(i-2)\),最后剩余的合法情况不难得出 \(f(i-2)\)

综上,\(f(i)=\mathrm{A}_{2^n-1}^{i-1}-f(i-1)+(i-1)\times (2^n-1-(i-2)\times f(i-2)\),除此之外考虑排列后的去重,直接乘上 \(m!\)\(\pmod{1e8+7}\) 定义下的逆元即可。

int main(){
    n=read(),m=read();
    power=(q_pow(2,n)-1+mod)%mod;
    for(int i=1;i<=m;i++){
        fac=fac*i%mod;
    }
    c[0]=1,dp[0]=1,dp[1]=0;
    for(int i=1;i<=m;i++){
        c[i]=c[i-1]*(power-i+1+mod)%mod;
    }
    for(int i=2;i<=m;i++){
        dp[i]=c[i-1];
        dp[i]=(dp[i]-dp[i-1]+mod-dp[i-2]*(i-1)%mod*(power-i+2)%mod+mod)%mod;
    }
    printf("%lld\n",dp[m]*q_pow(fac,mod-2)%mod);
    return 0;
}

5、中国剩余定理相关

1.中国剩余定理

求解如下的线性同余方程:

\[\begin{cases} x\equiv a_1\pmod{n_1}\\ x\equiv a_2\pmod{n_2}\\ \vdots\\ x\equiv a_k\pmod{n_k} \end{cases}\]

(\(n_1,n_2\dots n_k\) 两两互质)

求解步骤如下:

  • 求出所有模数的乘积 \(n=\prod_{i=1}^k n_i\)
  • 计算每一个方程的 \(m_i=\frac{n}{n_i}\),并求出 \(m_i\) 在模 \(n_i\) 定义下的逆元 \(m_i^{-1}\),并求出 \(c_i=m_im_i^{-1}\)(不取模);
  • 方程的唯一解 \(x=\sum_{i=1}^k a_ic_i \pmod{n}\)

证明如下:

把答案写成与方程系数相关的数应当为:

\[x=\sum_{i=1}^k a_i\times \frac{n}{n_i}\times inv(\frac{n}{n_i},n_i) \]

当这个式子代入 第 \(i\) 个方程时,考虑到 \(\frac{n}{n_i}\times inv(\frac{n}{n_i},n_i)\)\(\pmod{n_i}\) 下的值为 \(1\),因此答案为 \(a_i\times 1=a_i\)

而当带入到第 \(j \ (i\neq j)\) 个方程中,因为 \(\frac{n}{n_i} \bmod n_j\) 的值为 \(0\),所以对答案是没有贡献的。

例题

1.P1495 曹冲养猪

套板子……

inline ll exgcd(ll a,ll b,ll &x,ll &y){
    if(!b){
        x=1,y=0;
        return a;
    }
    ll d=exgcd(b,a%b,x,y);
    ll tmp=x;
    x=y,y=tmp-a/b*y;
    return d;
}
ll p[15],a[15],c[15];
ll px=1;
ll ans;
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        p[i]=read();
        a[i]=read();
        px*=p[i];
    }
    for(int i=1;i<=n;i++){
        ll x=0,y=0,tmp=px/p[i];
        exgcd(tmp,p[i],x,y);
        x=(x%p[i]+p[i])%p[i];
        c[i]=tmp*x;
    }
    for(int i=1;i<=n;i++){
        ans=(ans+a[i]*c[i])%px;
    }
    printf("%lld\n",ans);
    return 0;
}

2.P3868 TJOI2009 猜数字

单拿出一个式子 \((n-a_i)\mid b_i\),这个式子等价于 \(n-a_i \equiv0\pmod{b_i}\),也就是 \(n\equiv a_i\pmod{b_i}\),即整除式都等价于线性同余方程组,套板子即可。(需要龟速乘)

3.P2480 SDOI2010 古代猪文

\(g^{\sum_{d\mid n} \mathrm{C}_n^d} \bmod 9999116589\)

这个式子可以化成:\(g^{\sum_{d\mid n}\mathrm{C}_n^d \bmod 999911658} \bmod 999911659\)

\(999911658\) 这个数很奇怪对吧,发现 \(999911658=2\times 3\times 4679\times 35617\),所以我们列一个线性同余方程组:

\[\begin{cases} x\equiv \sum_{d\mid n} \mathrm{C}_n^d \pmod{2}\\ x\equiv \sum_{d\mid n} \mathrm{C}_n^d \pmod{3}\\ x\equiv \sum_{d\mid n} \mathrm{C}_n^d \pmod{4679}\\ x\equiv \sum_{d\mid n} \mathrm{C}_n^d \pmod{35617} \end{cases}\]

然后求出 \(x\) 并快速幂求解。

//2 3 4679 35617
ll n,g;
ll pr[5]={0,2,3,4679,35617};
ll modx=999911658;
ll fac[40005];
ll a[5];
inline ll q_pow(ll x,ll p,ll md){
    ll ans=1;
    while(p){
        if(p&1){
            ans=ans*x%md;
        }
        x=x*x%md;
        p>>=1;
    }
    return ans;
}
inline ll C(ll n,ll m,ll md){
    if(n

2.扩展中国剩余定理

求解如下的线性同余方程:

\[\begin{cases} x\equiv a_1\pmod{n_1}\\ x\equiv a_2\pmod{n_2}\\ \vdots\\ x\equiv a_k\pmod{n_k} \end{cases}\]

(\(n_1,n_2\dots n_k\) 不保证两两互质)

我们单单来看前两个式子,将 \(x\) 表示为 \(x=pn_1+a_1=qn_2+a_2\),整理一下就有 \(pn_1-qn_2=a_2-a_1\),首先可以得到当 \(\gcd(n_1,n_2)\mid (a_2-a_1)\) 时有解,而后解这个不定方程得出 \(x=pn_1+a_1\) 的值,随后代入到式子中,得出一个新的方程 \(x\equiv pn_1+a_1 \pmod{\operatorname{lcm}(n_1,n_2)}\),以此类推不断合并。

例题

1.P4777 扩展中国剩余定理(EXCRT)

ll n;
ll a[maxn],p[maxn];
inline ll exgcd(ll a,ll b,ll &x,ll &y){
	if(!b){
		x=1,y=0;
		return a;
	}
	ll ans=exgcd(b,a%b,x,y);
	ll tmp=x;
	x=y;
	y=tmp-a/b*x;
	return ans;
}
inline ll s_mul(ll x,ll p,ll md){
	ll ans=0;
	while(p){
		if(p&1){
			ans=(ans+x)%md;
		}
		x=(x+x)%md;
		p>>=1;
	}
	return ans;
}
inline ll excrt(){
	ll ans=a[1],px=p[1],x,y;
    for(int i=2;i<=n;i++){
        ll c=((a[i]-ans)%p[i]+p[i])%p[i];
        ll g=exgcd(px,p[i],x,y),md=p[i]/g;
        if(c%g) return -1;
        x=s_mul(x,c/g,md);
        ans+=x*px;
        px*=md;
        ans=(ans%px+px)%px;
    }
    return (ans%px+px)%px;
}
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        p[i]=read(),a[i]=read();
    }
    printf("%lld\n",excrt());
    return 0;
}

3.扩展卢卡斯定理

放在这里的原因是,这东西需要的前置知识太多了……

首先,目的是要求一个 \(\binom{n}{m} \mod P\),而 \(P\) 不一定为质数,简略的实现原理是把 \(P\) 唯一分解然后构造线性同余方程组,再用中国剩余定理合并并求出答案,下面是具体过程。

\(P\) 质因数分解使其等于 \(p_1^{\alpha_{1}}p_2^{\alpha_{2}}\cdots p_{k}^{\alpha_{k}}\),之后可以求出:

\[\begin{cases} \mathrm{C}_{n}^{m} \bmod p_1^{\alpha_{1}}\\ \mathrm{C}_{n}^{m} \bmod p_2^{\alpha_{2}}\\ \vdots\\ \mathrm{C}_{n}^{m} \bmod p_k^{\alpha_{k}}\\ \end{cases}\]

不难发现,这可以构造成线性同余方程组:

\[\begin{cases} \mathrm{C}_{n}^{m} \equiv a_1 \pmod{p_1^{\alpha_{1}}}\\ \mathrm{C}_{n}^{m} \equiv a_2 \pmod{p_2^{\alpha_{2}}}\\ \vdots\\ \mathrm{C}_{n}^{m} \equiv a_k \pmod{p_k^{\alpha_{k}}}\\ \end{cases}\]

只需要求出 \(a_1,a_2\dots a_k\) ,就能通过中国剩余定理合并得到结果。(明显这 \(k\) 个模数是两两互质的)

之后我们单单拿出一个式子来看 \(\mathrm{C}_{n}^m \bmod p^\alpha\),也就是求:

\[\dfrac{n!}{m!(n-m)!} \bmod p^\alpha \]

这里由于 \(p^\alpha\) 并非对所有数都存在逆元,所以我们把式子写成:

\[\dfrac{\dfrac{n!}{p^{g(n)}}}{\dfrac{m!}{p^{g(m)}}\dfrac{(n-m)!}{p^{g(n-m)}}}\times p^{g(n)-g(m)-g(n-m)} \bmod p^\alpha \]

其中 \(g(n)\) 表示 \(n\) 中所含 \(p\) 质因子的个数。

不难发现,分子和分母的三个分式都是等价的,只需研究:

\[\dfrac{n!}{p^{g(n)}} \bmod p^\alpha \]

而后因为 \(n!\) 中必然含 \(\lfloor \frac{n}{p}\rfloor\)\(p\) 质因子,所以可以变形成如下形式:

\[\begin{aligned} n!&=1\times 2\times \cdots \times n\\ &=(p\times 2p\times \cdots \times \lfloor \frac{n}{p}\rfloor\times p)\times (1\times 2\times \cdots \times n)\\ &=p^{\lfloor \frac{n}{p}\rfloor}\times (1\times 2\times \cdots\times \lfloor \frac{n}{p}\rfloor)\times \left(\prod_{i=1,i\not\equiv0\pmod{p}}^n i\right)\\ &=p^{\lfloor \frac{n}{p}\rfloor}\times (\lfloor \tfrac{n}{p}\rfloor)!\times \left(\prod_{i=1,i\not\equiv0\pmod{p}}^n i\right) \end{aligned}\]

显然后面的求积是可以每几个构成一组,实际是相同的 \(\lfloor \frac{n}{p^\alpha}\rfloor\) 组以及一组剩余数字,就有:

\[\begin{aligned} n!&=p^{\lfloor \frac{n}{p}\rfloor}\times(\lfloor \tfrac{n}{p}\rfloor)!\times \left(\prod_{i=1,i\not\equiv0\pmod{p}}^n i\right)\\ &=p^{\lfloor \frac{n}{p}\rfloor}\times (\lfloor \tfrac{n}{p}\rfloor)!\times \left(\prod_{i=1,i\not\equiv0\pmod{p}}^{p^\alpha} i\right)^{\tfrac{n}{p^\alpha}}\times \left(\prod_{i=p^\alpha\cdot\lfloor\tfrac{n}{p^\alpha}\rfloor,i\not\equiv0\pmod{p}}^n i\right) \end{aligned}\]

我们令 $f(n)=\dfrac{n!}{p^{g(n)}} $,由于式子中的 \(p^{\lfloor \frac{n}{p}\rfloor}\),是可以消掉的,因此把式子化简成:

\[f(n)=f(\lfloor\tfrac{n}{p}\rfloor)\times \left(\prod_{i=1,i\not\equiv0\pmod{p}}^{p^\alpha} i\right)^{\tfrac{n}{p^\alpha}}\times \left(\prod_{i=p^\alpha\cdot\lfloor\tfrac{n}{p^\alpha}\rfloor,i\not\equiv0\pmod{p}}^n i\right) \]

接下来看前面的 \(g(n)\),会影响到的也就是 \(p^{\lfloor \frac{n}{p}\rfloor}\times(\lfloor \tfrac{n}{p}\rfloor)!\) 的结果,因此需要递归求解,即:

\[g(n)=\lfloor\tfrac{n}{p}\rfloor +g(\lfloor\tfrac{n}{p}\rfloor) \]

回带到最后一个式子中:

\[\operatorname{ans}=\dfrac{f(n)}{f(m)f(n-m)} \times p^{g(n)-g(m)-g(n-m)} \bmod p^\alpha \]

求解就可以了。(大喘气)

例题

1.P4720 扩展卢卡斯定理/exLucas

inline ll q_pow(ll x,ll p,ll md){
    ll ans=1;
    x%=md;
    while(p){
        if(p&1){
            ans=ans*x%md;
        }
        x=x*x%md;
        p>>=1;
    }
    return ans;
}
inline ll exgcd(ll a,ll b,ll &x,ll &y){
    if(!b){
        x=1,y=0;
        return a;
    }
    ll d=exgcd(b,a%b,x,y);
    ll tmp=x;
    x=y,y=tmp-a/b*y;
    return d;
}
inline ll get_inv(ll a,ll md){
    ll x,y;
    exgcd(a,md,x,y);
    return (x%md+md)%md;
}
inline ll f(ll n,ll md,ll px){
    if (!n) return 1;
    ll num1=1,num2=1;
    for(ll i=1;i<=px;i++){
        if(i%md){
            num1=num1*i%px;
        }
    }
    num1=q_pow(num1,n/px,px);
    for(ll i=px*(n/px);i<=n;i++){
        if(i%md){
            num2=num2*(i%px)%px;
        }
	}
    return f(n/md,md,px)*num1%px*num2%px;
}
inline ll g(ll n,ll md){
    if(n1){
        a[++tot]=C(n,m,tmp,tmp);
        b[tot]=tmp;
    }
    ll ans=0;
    for(ll i=1;i<=tot;i++){
        ll tmpx=md/b[i],inv=get_inv(tmpx,b[i]);
        ans=(ans+a[i]*tmpx%md*inv%md)%md;
    }
    return ans;
}
ll n,m,p;
int main(){
    n=read(),m=read(),p=read();
    printf("%lld\n",exLucas(n,m,p));
    return 0;
}

2.P2183 礼物

式子是非常好推的,我们记 \(sum=\sum_{i=1}^m w(i)\),那么分两种情况讨论

  • \(sum>n\) 则礼物一定是不够分的,输出“\(\text{Impossible}\)”;
  • 否则我们看任意一组个数为 \(sum\) 的组,方案数为 \(\mathrm{C}_{sum}^{w(1)}\cdot \mathrm{C}_{sum-w(1)}^{w(2)}\cdots \mathrm{C}_{w(m)}^{w(m)}\),而这样的组一共有 \(\mathrm{C}_{n}^{sum}\)个,答案乘上即可。

考虑到模数不一定未知数,需要扩展卢卡斯。

int main(){
    md=read();
    n=read(),m=read();
    for(int i=1;i<=m;i++){
        w[i]=read();
        sum+=w[i];
    }
    if(sum>n){
        printf("Impossible\n");
        return 0;
    }
    ans=exLucas(n,sum,md);
    for(int i=1;i<=m;i++){
        ans=ans*exLucas(sum,w[i],md)%md;
        sum-=w[i];
    }
    printf("%lld\n",ans);
    return 0;
}

6、矩阵乘法相关

对于 \(n\times m\) 的矩阵 \(A\)\(m\times p\) 的矩阵 \(B\),相乘后就有:

\[C_{i,j}=\sum_{k=1}^m A_{i,k}\times B_{k,j} \]

矩阵乘法常用于优化递推式,尤其是重复的累乘问题。

例题

1.P1962 斐波那契数列

如果直接递推 \(fib(i)=fib(i-1)+fib(i-2)\) 是绝对会超时的,我们将矩阵 \(\begin{bmatrix}fib(n-1)&fib(n-2)\end{bmatrix}\),通过乘法转移到 \(\begin{bmatrix}fib(n)&fib(n-1)\end{bmatrix}\),因为 \(f(n)=1\times f(n-1)+1\times f(n-2),f(n-1)=1\times f(n-1)+0\times f(n-2)\),因此可以写为:

\[\begin{bmatrix}f(n-1)&f(n-2)\end{bmatrix}\times \begin{bmatrix}1&1\\1&0\end{bmatrix}=\begin{bmatrix}f(n)&f(n-1)\end{bmatrix} \]

不难发现这个可以用快速幂优化,设这个矩阵为 \(f\),最终结果就是 \(\begin{bmatrix}f(n-1)&f(n-2)\end{bmatrix}\times f^{n-2}\)

struct mat{
    ll num[3][3];
    mat operator *(const mat &tmp)const{
        mat res;
        memset(res.num,0,sizeof(res.num));
        for(int i=1;i<=2;i++){
            for(int j=1;j<=2;j++){
                for(int k=1;k<=2;k++){
                    res.num[i][j]=(res.num[i][j]+num[i][k]*tmp.num[k][j]%m)%m;
                }
            }
        }
        return res;
    }
}f,ans;
inline mat q_pow(mat x,ll p){
    mat ans=x;
    p--;
    while(p){
        if(p&1){
            ans=ans*x;
        }
        x=x*x;
        p>>=1;
    }
    return ans;
}
int main(){
    f.num[1][1]=1,f.num[1][2]=1,f.num[2][1]=1,f.num[2][2]=0;
    ans.num[1][1]=ans.num[1][2]=1;
    n=read();
    if(n<=2){
        printf("1\n");
    }
    else{
        ans=ans*q_pow(f,n-2);
        printf("%lld\n",ans.num[1][1]);
    }
    return 0;
}

2.P4159 SCOI2009 迷路

考虑如果矩阵是 \(0-1\) 的那么只需要求矩阵 \(f\)\(t\) 次幂,又发现矩阵只含 \([0,9]\) 的元素,考虑拆点,快速幂即可。

3.佳佳的 Fibonacci

首先令 \(S(i)=\sum_{j=1}^i F(i)\)\(P(i)=\sum_{j=1}^i S(i)\),不难发现,\(P(i)=\sum_{j=1}^i (n-i+1)\times F(i)= (i+1)\times S(i)-T(i)\),考虑用矩阵乘法去优化。
可以发现的是:

\[\begin{cases} F(i+1)&=1\times F(i)&+1\times F(i-1)&+0\times S(i)&+0\times P(i)\\ F(i)&=1\times F(i)&+0\times F(i-1)&+0\times S(i)&+0\times P(i)\\ S(i+1)&=1\times F(i)&+1\times F(i-1)&+1\times S(i)&+0\times P(i)\\ P(i+1)&=1\times F(i)&+1\times F(i-1)&+1\times S(i)&+1\times P(i)\\ \end{cases}\]

于是得到:
\(\begin{bmatrix}1&1&0&0\\1&0&0&0\\1&1&1&0\\1&1&1&1\end{bmatrix}\) 的矩阵,快速幂即可。
最终答案 \(T(n)=(n+1)\times S(n)-P(n)\)

ll n,m;
struct mat{
    ll num[5][5];
    mat operator *(const mat &tmp)const{
        mat res;
        memset(res.num,0,sizeof(res.num));
        for(int i=1;i<=4;i++){
            for(int j=1;j<=4;j++){
                for(int k=1;k<=4;k++){
                    res.num[i][j]=(res.num[i][j]+num[i][k]*tmp.num[k][j]%m)%m;
                }
            }
        }
        return res;
    }
}f,ans;
inline mat q_pow(mat x,ll p){
    mat ans=x;
    p--;
    while(p){
        if(p&1){
            ans=ans*x;
        }
        x=x*x;
        p>>=1;
    }
    return ans;
}
int main(){
    n=read(),m=read();
    if(n==1){
        ll res=1%m;
        printf("%lld\n",res);
    }
    else if(n==2){
        ll res=3%m;
        printf("%lld\n",res);
    }
    else{
        f.num[1][1]=1,f.num[1][2]=1,f.num[1][3]=0,f.num[1][4]=0;
        f.num[2][1]=1,f.num[2][2]=0,f.num[2][3]=0,f.num[2][4]=0;
        f.num[3][1]=1,f.num[3][2]=1,f.num[3][3]=1,f.num[4][3]=0;
        f.num[4][1]=1,f.num[4][2]=1,f.num[4][3]=1,f.num[4][4]=1;
        ans.num[1][1]=1,ans.num[2][1]=0,ans.num[3][1]=1,ans.num[4][1]=1;
        ans=q_pow(f,n-1)*ans;
        printf("%lld\n",(((n+1)*ans.num[3][1]%m)-ans.num[4][1]+m)%m);
    }
    return 0;
}