同余小结

引言

形如 axb(mod m) 同余方程的求解,以及同余定理的应用常常隐藏在算法竞赛的很多题目里。

为什么取余满足加法,减法和乘法而不满足除法?
为什么求解逆元的多种方法有不同的条件,而且如何认识方法的正确性?
如何理解九余数定理?
…..

通过对同余的了解和对同余定理的学习,我们能够对很多常见的结论有更深的认识。
同时有助于对其他相关算法的学习。

因本蒟蒻水平有限,此博客只作一些简陋的总结。若有错误之处还请指出~


同余的概念

对于同余,常见的下面这个式子: ab(mod m)

其数学意义是 a%m==b%m ,即 a,b m 的模相同。

因为 a,b 对m取余后有相同的余数
故若 a,b 做差,则相同的余数一定会被抵消掉,则满足:

(ab)%m==0

假设 (ab)==mk(kZ)
则上述同余式可化成:

a=b+mk

这也是做题时常使用的一种转化形式。


同余的性质

首先是学过离散的同学都知道的三大基本性质:自反性,对称性,传递性,即:
(1) aa(mod m)
(2)若有 ab(mod m) ,则 ba(mod m)
(3)若有 ab(mod m) bc(mod m) ac(mod m)

然后又是三条用于运算,解释了取模满足加法,减法和乘法的性质:
若有 ab(mod m) cd(mod m)
(4) (a+c)(b+d)(mod m)
(5) (ac)(bd)(mod m)
(6) (ac)(bd)(mod m)

而为什么除法不满足取模呢,那是因为除法满足的同余性是:
(7)若 acbc(mod m)

ab(modmgcd(c,m))


简单证明瞎扯一下:

acbc(mod m)

转化后得出:
ac=bc+mk

两边同除c:
a=b+mkc

k 为任意整数,而模数也应该是任意小的整数
故可转化为:

a=b+mgcd(m,c)k

ab(modmgcd(c,m))


另外再补充几个重要性质:
(8)若有 ab(mod m) anbn(mod m)
(9)若有 ab(mod m) n|m ab(mod n)


九余数定理 and 弃九法

以上两个数论知识就不详细介绍了,来看这么一道题。
HDU 1163

题意:给你一个正整数n,每次将所有位数加起来得到一个新的数,直到新的数只有一位。输出最后的结果。

思路:我们举 623 为例来看一下:(偷偷膜一发神犇czy623)
623>11>2

让我们来思考一下到底发生了什么。

把每个数按十进制展开:
6100+210+3>6+2+3=11
每一次操作实质上是将每一位的权值(十位的10,百位的100)变成了1。
如果只是将10变成1,我们可以考虑:
10/10=1
109=1
10%9=1
….
但是,我们同时需要将100,1000甚至100000….都变成1,所以,只有对九取模才能达到这样的效果。
如果学过离散数学的同学就能发现,在这里 10n 变成了一个等价类,只有同余关系满足自反,对称和传递性,所以才能满足条件。

所以本题只需要求出该数对9取模的数就好了。
另外还有一个细节便是n全是正整数,所以最终答案应该是1~9。但
9%9=0 所以应该特判答案为0的情况。

代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;

const int mod = 9;
int main(){
    int n;
    while(~scanf("%d",&n) && n){
        int ans = 1;
        for(int i=1 ;i<=n ;i++) ans = (ans*n)%mod;
        if(ans == 0) puts("9");
        else         printf("%d\n",ans);
    }
    return 0;
}

由上题我们可以得出一个结论:
一个数对9取余 <==> 该数所有数位的和对9取余。

所以我们又可以解决下面这道题:
HPU 1305
简单思考一下就能轻松过掉啦~

参考代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;

const int mod = 9;
const int A = 1e6 + 10;
char s[A];

int main(){
    int q;
    while(~scanf("%s%d",s,&q)){
        int sum = 0;
        int len = strlen(s);
        for(int i=0 ;i<len ;i++){
            sum = (sum + s[i] - '0')%mod;
        }

        while(q--){
            int pos,val;
            scanf("%d%d",&pos,&val);
            sum -= s[pos-1] - '0';
            sum = ((sum+val)%mod + mod)%mod;
            s[pos-1] = val + '0';
            if(sum == 0) puts("1");
            else         puts("0");
        }
    }
    return 0;
}

巧合的是,一个数对 3 取余也等价于该数所有数位的和对 3 取余。
仔细思考我们发现,在十进制下, 139 均满足上述的性质。

那么在任意的 P 进制下呢?有多少个数满足该性质。
这就是2017年百度之星初赛(A)的一道题:题目链接 HDU 6108

我们可以试着用同余的性质来解决该问题。
假设 x P 进制下满足该性质的一个数。
我们仍然用 623 进行举例:

623 % x=(6P2+2P+3) %x=(6+2+3)% x

由乘法的同余性可见:
P%x=1

即:
(P1) % x=0

P 进制下(P-1)的约数均满足该性质。
对于此题,也就转化成了求约数个数的经典问题。

代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<map>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;

const int A = 1e5 + 10;
bool vis[A];
int pri[A],tot;

void init(){
    tot = 0;
    for(int i=2 ;i<A ;i++){
        if(vis[i] == 0) pri[++tot] = i;
        for(int j=1 ;j<=tot && i*pri[j]<A ;j++){
            vis[i*pri[j]] = 1;
            if(i%pri[j] == 0) break;
        }
    }
}

int main(){
    init();
    int T;
    scanf("%d",&T);
    while(T--){
        int n;
        scanf("%d",&n);
        n--;
        ll ans = 1;
        for(int i=1 ;i<=tot && pri[i]<=n ;i++){
            if(n % pri[i] == 0){
                int cnt = 0;
                while(n%pri[i] == 0){
                    n /= pri[i];
                    cnt++;
                }
                ans *= (cnt+1);
            }
        }
        if(n>1) ans*=2;
        printf("%I64d\n",ans);
    }
    return 0;
}

乘法逆元

此节可参考我以前的两篇博客:
乘法逆元小结
拓欧详解

但当所求模不是素数时,可以尝试下面的转换:

AB%mod=A%(Bmod)B

下面又是一波看似很有道理的证明:

假设:

Ans=AB%mod

则可转化为:
AB=modk+Ans(kZ)

两边同乘B,得:

A=(Bmod)k+AnsB

即:
AnsB=A%(Bmod)

将B除到等式右边,易知:
Ans=A%(Bmod)B

证毕。


线性同余方程组

下面介绍中国剩余定理:
形如下面的同余方程组。

xxx...xa1a2a3an(mod(mod(mod(modm1)m2)m3)mn)

m1,m2,m3....mn 两两互质。
则该方程组在 M=m1m2m3...mn 下的解唯一,且:
x=(a1M1M11+a2M2M12+....+anMnM1n)%M
其中: Mi=M/mi M1i Mi 的逆元。

下面是一道例题。
POJ 1006
中国剩余定理裸题
代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;

const int A = 5;
int a[A],m[A];
int M;

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

int CRT(int a[],int m[],int n){
    M = 1;
    for(int i=1 ;i<=n ;i++) M*=m[i];
    int ans = 0;
    for(int i=1 ;i<=n ;i++){
        int x,y;
        int M_i = M / m[i];
        exgcd(M_i,m[i],x,y);
        x = (x%m[i] + m[i])%m[i];
        ans = (ans + a[i]*M_i*x)%M;
    }
    ans = (ans+M)%M;
    return ans;
}

int main(){
    int p,e,i,d,_=1;
    while(~scanf("%d%d%d%d",&p,&e,&i,&d)){
        if(p==-1&&e==-1&&i==-1&&d==-1) break;
        a[1] = p,a[2] = e,a[3] = i;
        m[1] = 23,m[2] = 28,m[3] = 33;
        int ans = ((CRT(a,m,3) - d)%M + M)%M;
        if(ans == 0) ans = M;
        printf("Case %d: the next triple peak occurs in %d days.\n",_++,ans);
    }
    return 0;
}

而因 mi 均是已知的值,故我们可以提前将 MiM1i 其预处理出来,从而快速求解。
代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;

const int mod = 23*28*33;

int main(){
    int a = 5544,b = 14421,c = 1288;
    int p,e,i,d,_=1;
    while(~scanf("%d%d%d%d",&p,&e,&i,&d)){
        if(p==-1&&e==-1&&i==-1&&d==-1) break;
        int ans = ((p*a+e*b+c*i-d)%mod + mod)%mod;
        if(ans == 0) ans = mod;
        printf("Case %d: the next triple peak occurs in %d days.\n",_++,ans);
    }
    return 0;
}

但当 m1,m2.....mn 不互质时,我们就需要用更加通用的方法来解同余方程组。
考虑下面的两个同余方程:

{xxa1a2(mod(modm1)m2)

转化后得到:

{xx==a1a2++m1k1m2k2

化简得:
a1+m1k1=a2+m2k2
移项得:
m1k1m2k2=a2a1

由拓展欧几里德,知:
(a2a1) % gcd(m1,m2)!=0 则此方程无解。
否则应用拓展欧几里德解出 k1 后,代入 x=a1+m1k1
解出 x

故最后可合并为:
xx(mod  lcm(m1,m2))

下面是两道例题:
POJ 2891

代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;

const int A = 1e3 + 10;
ll a[A],m[A];

ll exgcd(ll a,ll b,ll& x,ll &y){
    if(b == 0){
        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 CRT(ll a[],ll m[],int n){
    ll M = m[1],R = a[1];
    for(int i=2 ;i<=n ;i++){
        ll x,y;
        ll d = exgcd(M,m[i],x,y);
        if((a[i]-R) % d) return -1;
        x = ((x*(a[i]-R)/d)%(m[i]/d) + (m[i]/d))%(m[i]/d);
        R += M*x;
        M = M*m[i]/d;
        R = (R%M+M)%M;
    }
    return R;
}

int main(){
    int n;
    while(~scanf("%d",&n)){
        for(int i=1 ;i<=n ;i++) scanf("%I64d%I64d",&m[i],&a[i]);
        printf("%I64d\n",CRT(a,m,n));
    }
    return 0;
}

HDU 1573
模板题
代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;

const int A = 10 + 10;
int a[A],m[A];

int exgcd(int a,int b,int& x,int &y){
    if(b == 0){
        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 CRT(int a[],int m[],int n,int &M){
    int R = a[1];
    M = m[1];
    for(int i=2 ;i<=n ;i++){
        int x,y;
        int d = exgcd(M,m[i],x,y);
        if((a[i]-R) % d) return -1;
        x = ((x*(a[i]-R)/d)%(m[i]/d) + (m[i]/d))%(m[i]/d);
        R += M*x;
        M = M*m[i]/d;
        R = (R%M+M)%M;
    }
    return R;
}

int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        int all,n,M;
        scanf("%d%d",&all,&n);
        for(int i=1 ;i<=n ;i++) scanf("%d",&m[i]);
        for(int i=1 ;i<=n ;i++) scanf("%d",&a[i]);
        int ans = CRT(a,m,n,M);
        if(ans == -1 || ans>all) puts("0");
        else{
            all -= ans;
            int num = all/M;
            if(ans>0) num++;  //所求的是正整数个数
            printf("%d\n",num);
        }
    }
    return 0;
}

下面是中国剩余定理的另一个重要应用-大数分解和合并
这也是在计算机科学中常见的应用。
假定 m1,m2...mn 为两两互质且大于等于2的整数,令:
M=m1m2....mn
由中国剩余定理知:
对于 0<=x<M 的任意整数 x ,假定 ai 满足 ai=x%mi ,则所有 x 均可以用 (m1,a1)(m2,a2)....(mn,an) n个二元对唯一表示。

举个例子:
现有 m1=99  m2=98  m3=97  m4=95
M=m1m2m3m4=89403930

比如我们现在要计算:
123684+413456=
我们可以先将两个数表示成他们对 mi 取余的情况。
因为
123684%99=33
123684%98=8
123684%97=9
123684%95=89
123684=(33,8,9,89)
同理
413456=(32,92,42,16)

当求和时,只需要以下步骤:
(33,8,9,89)+(32,92,42,16)=(65%99,100%98,51%97,105%95)=(65,2,51,10)

因为每一个四元坐标对应唯一一个小于M的整数 x
故可用中国剩余定理求出:
(62,2,51,10)=537140 故和即为所求。

下面是一道例题:
HDU 4767

此题叫我们求出模 P=95041567 的第n项Bell数。
因为n的范围很大,按照常规办法预处理第二类斯特林数求和或者打表一个贝尔三角形在时间和空间复杂度两方面均是爆炸的。

所以我们可以考虑利用Bell数的两个重要的同余性质。( P 为素数)
Bp+nBn+Bn+1(mod P)
Bpm+nmBn+Bn+1(mod P)

此时我们可以注意到题目给了一个奇怪的 P=95041567
虽然取模一般套路上都是一个大素数 1e9+7
但此题作为亚洲区域赛的题目, 给了这么一个奇怪的模数,说明切题点大概率就在这个P上。
分解因子后易得 P=3137414347
故由我们上面介绍的例子,我们可以分别求出 Bell(n)%{31,37,41,43,47} 的值,然后通过中国剩余定理进行合并,答案即为所求。
而每个模数 p 都很小,故可以利用同余性质快速求解。
利用性质1可构建矩阵使用矩阵快速幂
而性质2可将n转化为 p 进制数,从而快速求解。

此为利用性质2的代码:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
using namespace std;
typedef long long ll;

const int P = 95041567;
const int N = 50;
int a[5] = {31,37,41,43,47};
int f[N],s[2][N];
int i,j,x;
ll n;

void init(){
    f[0] = f[1] = s[0][0] = 1;
    s[0][1]= 2;
    for(i=2,x=1;i<N ;i++,x^=1) for(f[i]=s[x][0]=s[x^1][i-1],j=1;j<=i;j++)
        s[x][j] = (s[x^1][j-1] + s[x][j-1])%P;
}
ll cal(int x,ll n){
    int m = 0,b[N],c[N],d[70];
    for(i=0 ;i<=x ;i++) b[i] = f[i]%x;
    while(n) d[m++] = n%x,n/=x;     //将n转化为x进制
    for(i=1 ;i<m ;i++) for(j=1 ;j<=d[i] ;j++){
        for(int k=0 ;k<x ;k++) c[k] = (b[k]*i + b[k+1])%x;
        c[x] = (c[0]+c[1])%x;
        for(int k=0 ;k<=x ;k++) b[k] = c[k];
    }
    return c[d[0]];
}
ll pow(ll a,ll b,ll p){ll t=1;for(a%=p;b;b>>=1LL,a=a*a%p) if(b&1LL) t=t*a%p;return t;}
ll bell(ll n){
    if(n<N) return f[n];
    ll t = 0;
    for(int i=0;i<5;i++) t = (t + (P/a[i])*pow(P/a[i],a[i]-2,a[i])%P*cal(a[i],n)%P)%P;  //CRT合并
    return t;
}

int main(){
    init();
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%I64d",&n);
        printf("%I64d\n",bell(n));
    }
    return 0;
}

参考资料:
西南大学本科毕业论文
ACdreamer

猜你喜欢

转载自blog.csdn.net/wubaizhe/article/details/76148514