初等数论与算法(一):整除、最大公约数、最小公倍数、辗转相除法、裴蜀定理


镇楼图

Pixiv:torino

注:有关程序部分均用C with STL描述;部分有数学证明,我个人认为过于简单或不常用的结论我不会进行证明,可以at我,我可以提供证明



一、整除、gcd与lcm

整除性

关于整数的加法、减法、乘法其运算都会保证结果都还在整数范畴呢,但唯独除法会产生其他结果

整除性是指b能整除a,即\(?q∈Z,a=bq\),记作\(b~|~a\)\(b≠0\)

其中\(b\)称为\(a\)的因数,\(a\)\(b\)的倍数

\(\not ?q∈Z\),即\(b\)不整除\(a\),记作\(b~|\not~~a\)

整除性的性质

■若\(c~| ~b,b~| ~a\),则\(c~|~a\)(传递性)

■若\(b~|~a\),则\(?k∈Z^*,kb~|~ka\)

■若\(kb~|~ka\),则\(b~|~a\)

■若\(c~|~a,c~|~b\),则\(m,n∈Z,c~|~ma+nb\)

■若\(b~|~a,a≠0\),则\(|b|≤|a|\)

■若\(b~|~a,a≠0\),则\(\frac{a}{b}~|~a\)

■若\(b~|~0\),则\(?q∈Z\)

余数

余数定义:若\(?a,b∈Z,b>0\),则\(?!q,r∈Z,a=bq+r\)\(r∈[0,b)\)

其中\(r=_b\)称为\(a\)整除\(b\)的非负最小剩余(余数)即\(r=a\%b\),当除数一致时可省略

\(=<+>\)

\(=<>\)

最大公因数

公因数定义:假设有一组正整数\(a_1,a_2,...,a_n\),若\(d~|~a_1,d~|~a_2,...,d~|~a_n\)则称\(d\)\(a_1,a_2,...,,a_n\)的公因数

最大公因数定义:假设有一组正整数\(a_1,a_2,...,a_n\),若\(d_1,d_2,...,d_n\)是这一组数的公因数,则\((a_1,a_2,...,a_n)=max{d_1,d_2,...,d_n}\)称为最大公因数,简记为\(m\)

\(m~|~a_i\)

\(1≤m≤min(a_i)\)

\(d_i~|~m\)(最大公约数是其他公约数的约数)

\((a,b)≥n\),当且仅当\(\begin{cases}n~|~a \\ n~|~b \end{cases}\)(证明要用到的重要结论!!!)

这个性质不难理解,由公因数的定义可得,因为n是公因数,那么肯定是要小于等于最大公因数的。但这里存在一个转换思路的过程。也就是如果要证明有关不等式的问题,可以将这个问题转成整除问题

■若\(m~|~a,m~|~b\),则\(m~|~(a,b)\)

\((a,b)~|~a,(a,b)~|~b\)

■若\(a=bq+r\)即满足余数定义且\(a,b∈N_+\),则\((a,b)~=~(b,r)=r_n\)(欧几里得算法)

证明:

? 要证明\((a,b)~=~(b,r)\)即证明\(\begin{cases} (a,b)≤(b,r) \\ (b,r)≤(a,b) \end{cases}\)

? 这里只说明其中一条其余可自证

? 由整除性第二条性质得要证明\((a,b)≤(b,r)\)即证明\(\begin{cases}(a,b)≤b \\ (a,b)≤r \end{cases}\)

? \(∵(a,b)~|~b\)

? \(∴(a,b)≤b\)

? \(∵r=a-bq\)

? \(∴(a,b)~|~(a-bq)\)(整除性第四条性质,已略部分步骤)

? \(∴(a,b)≤(b,r)\)

? 同理可得\((b,r)≤(a,b)\)

? \(∴(a,b)=(b,r)\)

比如要求解\((a,b)\)可以‘先求解\((b,r)\)直到最后\(r\)变小至0即可,在后面详细描述了欧几里得算法的实现即到最后会得到一个\((x,0)\)的一组数,那么\(x=(a,b)\)

■若\(a,b∈N_+\),则\((a,b)=ma+nb\)

证明:

? 本质上是欧几里得算法的逆过程

? \(a = bq_1+r_1\)

? \(b = r_1q_2 + r_2\)

? \(...\)

? 可以用一个四元组表示这四个数

? \(→...→\)

? \(r_{n+1} = 0\)

? 其中定义\(a,b,r_i\)前的系数统一定为\(C\)

(注意引入的\(C\)是一个表示任意的常数,C和C不一定相等)

? \(r_{n-1}=Cr_n\)

? \(r_{n-2}=Cr_n+r_n\)

? \(r_{n-3}=Cr_n+r_{n-1}\)

? \(r_{n-4}=Cr_{n}+Cr_{n-1}+r_{n-2}=Cr_n\)

? 简单来说就是不管是第几项都可以通过不断回代得到\(Cr_n\)至于\(C\)为多少需要计算

? 那么在第一项就可以这么表达

? \(a=Cb+r_1=Cb+Cr_n\)\(r_n=Ca+Cb=ma+nb\)

? 由此得证

? 具体算法求解m、n贴在了后面

■绝对最小剩余:若满足余数定义\(a=bq+r\)\(|r|≤\frac{b}{2}\),此时\(|r|\)称为绝对最小剩余

在欧几里得算法上运用绝对最小剩余在某些情况下可以优化次数

但首先要将一个非负最小剩余处理为绝对最小剩余

\(\begin{cases}r,r ≤ b/2,a=qb+r \\ r-b,b/2

简单来说就是如果发现不满足绝对最小剩余,可以q+1后r再减去b从而得到一个满足绝对最小剩余的余数(这里不作证明)

具体算法参考后面,只是多了一个判断就能优化

欧几里得算法(辗转相除法)

问题描述:已知性质\((a,b)=(b,r)\),如何求解正整数\(a\)\(b\)的最大公因数?

数学解决

\((a,b)=(b,r_1)=(r_1,r_2)=...=(r_{n-1},r_{n})=(r_n,0)=r_n\)

\(a=bq_1+r_1\)

\(b=r_1q_2+r_2\)

...

\(r_{n-1}=r_nq_{n+1}+r_{n+1}\)

\(∵r_{n+1}=0\)

\(∴r_{n}=(r_n,0)=(r_{n-1},r_n)=...=(a,b)\)

程序解决

//假设a>b,即a为被除数,b为除数
int gcd(int a,int b){
    return (b) ? gcd(b,a%b) : a ;
}

绝对最小剩余优化欧几里得算法

绝对最小剩余在某些情况下可以优化近一半的次数

比如gcd(288,158)

简单来说就是发现不满足绝对最小剩余的条件

■q变为q+1

■r变为r-b

(但是传参时为正数所以具体实现上r要变成b-r)

int gcd(int a, int b) {
    int r = a % b;
    if (r > b / 2)r = (b-r);
    return (r) ? gcd(b, r) : b;
}

推广:求解多个数的最大公约数

上面已经知晓了欧几里得算法求解两个数的最大公约数,如果要求解多个,其实也相当简单

(1)定理说明

假设有n个数,\(a_1,a_2,...a_n\)

\((a_1,a_2)=d_2\)

\((d_2,a_3)=d_3\)

\(...\)

$(d_{n-1},a_n)=d_n $

\(∴(a_1,a_2,...,a_n)=(d_{n-1},a_n)=d_n\)

数学证明略, 比较简单,可以用数学归纳法证明

同理可以推广得到裴蜀定理

\((a_1,a_2,...,a_n)=∑x_ia_i\)

(3)程序求解

//已知int gcd(int a,int b);
//备注:保证n>=2且传入参数均正整数,不作异常判断
int gcd(vector n) {
    int len = n.size();
    int d = gcd(n[0], n[1]);
    for (int i = 2; i < len; i++) {
        d = gcd(d, n[i]);
    }
    return d;
}

裴蜀定理引入(又称扩展欧几里得算法)

先来个具体案例,假设要求a=288,b=158的公因数,先尝试用欧几里得算法得到具体过程

/*
288=1*158+130
158=1*130+28
130=4*28+18
28=1*18+10
18=1*10+8
10=8*1+2
8=2*4
此时(a,b)=(r(n),0)=2
*/

裴蜀定理相当于回代的过程

简单来说就是除数当作上一情况的余数做回代
如2是最后情况的除数,确实是倒数第二步的余数,同理可得其他情况
因此抽象成递推式得到

当前情况:\(r_{i-1}=r_iq_{i+1}+r_{i+1}\)

上一情况:\(r_{i-2}=r_{i-1}q_{i}+r_i\)

当前情况的除数是\(r_i\)

由上一情况转换得

\(r_{i}=r_{i-2}-q_{i}r_{i-1}\)

根据欧几里得算法我们是知道传入的前两个数的

即已知当前情况的\(r_{i-1},r_{i}\)和上一情况的\(r_{i-2},r_{i-1}\)

所以也就剩下\(q_i\)未知,因此可以求得

\(q_{i}=r_{i-2}/r_{i-1}\)(整除)

因此只要知道最基本的情况,不断求解\(q_i\)即可回到第一步

但现在还少点东西,之后算法的实现还会用到上述证明过程,先看转换的具体的过程

/*
2=10-8*1			(1)2是最初的除数

 =10-(18-10*1)*1		(2)转8
 =2*10-18
 
 =2*(28-18)-18			(3)转10
 =2*28-3*18
 
 =2*28-3*(130-4*28)		(4)转18
 =14*28-3*130
 
 =14*158-17*130			(5)转28
 
 =31*158-17*288			(6)转130

∴(a,b)=31b-17a
*/

反推来看,设系数\(x,y\)满足\(ax+by=(a,b)\)

扩展欧几里得算法即求解最大公约数的同时带出系数

裴蜀定理证明与算法实现

现在考虑一般的递推的具体情况

基本情况:

\((r_n,0)\)时,等式为\((a,b)=x_{n}r_{n}+y_n*0\)

此时\(x=1,y=0\)(理论上y可以为任何值,但为了方便,约定为0)

而回代的过程在于将当前情况的除数回代成为上一情况的余数

根据之前推导的\(r_{i}=r_{i-2}-q_{i}r_{i-1}\)\(q_{i}=r_{i-2}/r_{i-1}\)(整除)

\((a,b)=x_n(r_{n-2}-r_{n-1}q_n)\)

接下来只要一步一步推导即可,从\(r_n\)一直反推到\(b\)

递推情况:

已知在\((r_{i-1},r_{i})\)\((a,b)=x_{i-1}r_{i-2}+y_{i-1}r_{i-1}\)

根据推导对\(r_{i-1}\)展开

\((r_{i-2},r_{i-1})\)\((a,b)=x_{i-1}r_{i-2}+y_{i-1}(r_{i-3}-?\frac{r_{i-3}}{r_{i-2}}?r_{i-2})=y_{i-1}r_{i-3}+(x_{i-1}-?\frac{r_{i-3}}{r_{i-2}}?)r_{i-2}\)

再简化成系数的变化(重要结论!!!)

\(x_{i-2}=y_{i-1} \\ y_{i-2}=x_{i-1}-?\frac{r_{i-3}}{r_{i-2}}?y_{i-1}\)

得到了这个结论后就可以来实现算法了

int exgcd(int a, int b, int &x, int &y) {
    //递归,当b=0时即裴蜀定理的初始情况
    //大体功能不变,依然返回(a,b)
    if (!b) {
        x = 1;
        y = 0;//y值说过可以随意改
        return a;
    }
    int gcd = exgcd(b, a%b, x, y);
    int t = y;
    y = x - a / b * y;
    x = t;
    //简单来说就是求出(a,b)以及更新系数
    //更新前后的顺序自己需要捋顺
    //这属于程序问题而不是数学问题
    return gcd;
    //细节备注:算法是要先知道初始的x,y才能继续更新
    //因此先定义gcd达到初始情况定义x,y
}

最小公倍数

公倍数与公因数是相互对偶的概念,之间存在联系

公倍数定义:假设有一组正整数\(a_1,a_2,...,a_n\),若\(a_1~|~m,a_2~|~m,...,a_n~|~m\)则称\(m\)\(a_1,a_2,...,,a_n\)的公倍数

最小公倍数定义:假设有一组正整数\(a_1,a_2,...,a_n\),若\(m_1,m_2,...,m_n\)是这一组数的公倍数,则\([a_1,a_2,...,a_n]=min{m_1,m_2,...,m_n}\)称为最小公倍数,简记为\(n\)

定理

\(a_i~|~n\)

\(n≥max(a_i)≥1\)

\(n~|~m_i\)(其他公倍数是最小公倍数的倍数)

\([a,b]≤n\),当且仅当\(\begin{cases}a~|~n \\ b~|~n \end{cases}\)(证明要用到的重要结论!!!)

■若\(a~|~m,b~|~m\),则\([a,b]~|~m\)

\([a,b](a,b)=ab\)(重要结论!!!)

(2)数学证明

? 首先将形式转变:\((a,b)=\frac{ab}{[a,b]}\)

? 要证明这个即证明两方面\(\begin{cases}(a,b)≤\frac{ab}{[a,b]} \\ (a,b)≥\frac{ab}{[a,b]} \end{cases}\)

? (之前证明过类似的)

? 这里只证明其中一点,要证明\((a,b)≤\frac{ab}{[a,b]}\)即证明\(\begin{cases}\frac{ab}{[a,b]} ~|~a \\ \frac{ab}{[a,b]}~|~ b\end{cases}\)

? 同样只证明一个

? 由于这个式子非常特殊你可以轻易地写出

? \(a=(\frac{ab}{[a,b]})(\frac{[a,b]}{b})\)

? 而且对于这个式子\(\frac{ab}{[a,b]}\)是整数(根据公倍数性质得到)

? \(\frac{[a,b]}{b}\)是整数(根据公倍数性质得到)

? \(∴{[a,b]} ~|~a\)

? 同理可得...

? \(∴\frac{ab}{[a,b]}~|~a\)

? 对于\((a,b)≥\frac{ab}{[a,b]}\)

? 最大公因数的性质无法将不等式转换成一组整除

? 但替换后\([a,b]≥\frac{ab}{(a,b)}\)最小公倍数可以将不等式转换一组整除\(\begin{cases}a~|~\frac{ab}{(a,b)} \\ b~|~\frac{ab}{(a,b)} \end{cases}\)

? 同理可得...

? 因此证得公式

? 可以发现证明等于的问题基本就是等于转不等式,不等式再根据最大公因数或最小公倍数的特点转成整除问题即可。整除问题只要能找到整数和整的除数相乘得到整的被除数即可。因此转换成了最小的问题是不是整数的问题,这个不难证明

(3)程序求解

/*已知之前所写的函数*/
int lcm(int a,int b){
    return a/gcd(a,b)*b;
}

推广:求解多个数的最小公倍数

与之前类似

//备注:保证n>=2且传入参数均正整数,不作异常判断
int lcm(vector n) {
    int len = n.size();
    int m = lcm(n[0], n[1]);
    for (int i = 2; i < len; i++) {
        m = lcm(d, n[i]);
    }
    return m;
}

推广:裴蜀定理

同理最大公约数可以推广,自然其扩展的算法当然也可以,但计算时更加复杂,这里给出我个人的解答,不代表没有其他解法

假设有一堆数\(n[0],n[1],...,n[N]\)

推广的裴蜀定理就是\(∑_ix[i]n[i]=(n[0],n[1],...,n[N])\)

如何求解各个系数呢?

和推广的欧几里得算法一致,先算前两项,然后\(d\)作为参数计算后面的项

\(d=(n[0],n[1])\)

算出第一项并带出\(x[0],x[1]\)

即式子是\(d=x[0]n[0]+x[1]n[1]\)

然后将\(d\)看成一整项,求解\(d'=(d,n[2])\)

最后也会求出系数,d的系数定为combine

\(d'=combine*d+x[2]n[2]=combine*x[0]n[0]+combine*x[1]n[1]+x[2]n[2]\)

那么combine会作用到之前所求的所有数之上

...

//备注:保证n>=2且传入参数均正整数,不作异常判断
int exgcd(vector n, vector &x) {
    //x[i]存储n[i]的系数
    //需要保证n.size()=x.size()
    int len = n.size();
    
    int d = exgcd(n[0], n[1], x[0], x[1]);
    int combine;
    //combine用于指定联合的系数最后x[j]要乘上的
    for (int i = 2; i < len; i++) {
        d = exgcd(d, n[i], combine, x[i]);
        for (int j = 0; j < i; j++) {
            x[j] *= combine;
        }
    }
    return d;
}

本章结束代码

在每章结束后我会附上完整的代码你是可以直接运行的

而代码会是本章讲到的一个内容的完整版

//推广的裴蜀定理
#include
#include
#include
using namespace std;

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

//备注:不作异常判断
int exgcd(vector n, vector &x) {
    int len = n.size();
    
    int d = exgcd(n[0], n[1], x[0], x[1]);
    int combine;
    for (int i = 2; i < len; i++) {
        d = exgcd(d, n[i], combine, x[i]);
        for (int j = 0; j < i; j++) {
            x[j] *= combine;
        }
    }
    return d;
}

int main() {
    vector n;
    n.push_back(288);
    n.push_back(158);
    n.push_back(77);
    vector x(3, 0);
    cout << exgcd(n,x) << endl;
    cout << x[0] << "\t" << x[1] << "\t" << x[2] << endl;
    return 0;
}