扩展欧几里得算法求逆元_从辗转相除法到求逆元,数论算法初体验
今天是算法和數(shù)據(jù)結(jié)構(gòu)專題的第22篇文章,我們一起來聊聊輾轉(zhuǎn)相除法。
輾轉(zhuǎn)相除法又名歐幾里得算法,是求最大公約數(shù)的一種算法,英文縮寫是gcd。所以如果你在大牛的代碼或者是書上看到gcd,要注意,這不是某某黨,而是指的輾轉(zhuǎn)相除法。
在介紹這個(gè)算法之前,我們先來看下最大公約數(shù)問題。
暴力解法
這個(gè)問題應(yīng)該很明確了,我們之前數(shù)學(xué)課上都有講過。給我們紙筆讓我們求都沒有問題,分解因數(shù)找下共同的部分,很快就算出來了。但是用代碼實(shí)現(xiàn)怎么做呢?
用代碼實(shí)現(xiàn)的話,首先排除分解因數(shù)的方法。因?yàn)榉纸庖驍?shù)復(fù)雜度太高了,也很容易想明白,既然要分解因數(shù),那么首先需要獲得一定量的質(zhì)數(shù)吧。有了質(zhì)數(shù)之后還要遍歷質(zhì)數(shù),將整數(shù)一點(diǎn)一點(diǎn)分解,顯然很麻煩,還不如直接暴力了。暴力解法并不復(fù)雜,我們直接從1開始遍歷,記錄下來同時(shí)能夠整除這兩個(gè)數(shù)的最大數(shù)即可。我們暴力的范圍也不大,從1到n。
很容易寫出代碼:
def gcd(a, b): ret = 0 for i in range(min(a, b)): if a % i == 0 and b % i == 0: ret = i return ret這個(gè)很簡單,也許你可能還會想出一些優(yōu)化,比如說首先判斷一下a和b之間是否有倍數(shù)關(guān)系,如果有的話直接就可以得到結(jié)果了。再比如說我們i的遍歷范圍其實(shí)可以不用到min(a, b),如果a和b沒有倍數(shù)關(guān)系的話min(a, b) / 2就可以了。這些都是沒有問題的,但是即使加上了這些優(yōu)化依然改變不了這是一個(gè)O(n)算法的本質(zhì)。
比如說a是1e9,b是1e9-1,毫無疑問這樣的做法會超時(shí)。
輾轉(zhuǎn)相除法
接下來就輪到正主——輾轉(zhuǎn)相除法出場了,這個(gè)算法在《九章算術(shù)》當(dāng)中曾經(jīng)出現(xiàn)過,叫做更相減損術(shù)。不管叫什么,原理都是一樣的,它的最核心本質(zhì)是下面這個(gè)式子:
這個(gè)式子就是著名的歐幾里得定理,這里的r可以看成是a對b取余之后的結(jié)果,也就是說a和b的最大公約數(shù)等于b和r的最大公約數(shù)。這樣我們就把a(bǔ)和b的gcd轉(zhuǎn)移成了b和r,然后我們可以繼續(xù)轉(zhuǎn)移,直到這兩個(gè)數(shù)之間存在倍數(shù)關(guān)系的時(shí)候就找到了答案。
在我們寫代碼之前,我們先來看一下這個(gè)定理的證明。
我們假設(shè)u同時(shí)整除a和b,顯然這樣的u一定存在,因?yàn)閡至少可以是1,所以:
所以可以得到u也整除r,同樣我們可以證明能夠整除b和r的整數(shù)也可以整除a。我們假設(shè)v可以同時(shí)整除b和r:
這樣我們就得到了v也可以整除a。也就是說a和b的每一個(gè)因子都是b和r的因子,同樣b和r的每一個(gè)因子也是a和b的因子,那么可以得出a和b的最大公約數(shù)就是b和r的最大公約數(shù)。
以上就是歐幾里得定理的簡單證明,如果看不懂也沒有關(guān)系,我們記住這個(gè)定理的內(nèi)容就可以了。
接下來就是用代碼實(shí)現(xiàn)了,我們把這個(gè)公式套進(jìn)遞歸當(dāng)中非常容易:
def gcd(a, b): if a < b: a, b = b, a if a % b == 0: return b return gcd(b, a % b)我們首先判斷了a和b的大小關(guān)系,如果a小于b的話,我們就交換它們的值,保證a大于b。如果a和b取模的結(jié)果為0,那么說明a已經(jīng)是b的倍數(shù)了,顯然它們之間的最大公約數(shù)就是b。
但其實(shí)我們沒有必要判斷a和b的大小,我們假設(shè)a小于b,那么顯然a % b = a,于是會遞歸調(diào)用b和a % b,也就是b和a,也就是說算法會自動(dòng)調(diào)整兩者的順序。這么一來,這個(gè)代碼還可以進(jìn)一步簡化,只需要一行代碼。
def gcd(a, b): return a if b == 0 else gcd(b, a % b)所以聽到有人說自己用一行代碼實(shí)現(xiàn)了一個(gè)算法,不要覺得它在裝逼,有可能他真的寫了一個(gè)gcd。
拓展歐幾里得
拓展歐幾里得本質(zhì)上就是gcd,只是在此基礎(chǔ)上做了一定的拓展,從而來解決不定方程。不定方程就是ax + by = c的方程,方程要有解充要條件是(a, b) | c,也就是說a和b的最大公約數(shù)可以整除c。
也就是說求解ax + by = gcd(a, b)的解。假如說我們找到了這樣一組解x0和y0,那么x0 + (b / gcd) * t和y0 - (a / gcd) * t也是方程的解,這里的t可以取任意整數(shù)。
我們代入算一下即可:
所以我們求出了這樣的x0和y0之后就相當(dāng)于求出了無數(shù)組解,那么這個(gè)x0和y0怎么求呢,這就需要用到gcd算法了。
我們觀察一下gcd算法的遞歸代碼,可以發(fā)現(xiàn)算法的終止條件是a=gcd,b=0。對于這樣的a和b來說,我們已經(jīng)找到了一組解使得ax+by=gcd,比如很明顯,x=1,y=0。實(shí)際上y可以為任何值,因?yàn)閎=0。
我們回到遞歸的上一層的a和b,假設(shè)我們已經(jīng)求出了b和a%b的最大公約數(shù),并且求出了一組解x0和y0。使得b*x0 + (a%b)* y0 = gcd。那么我們能不能倒推得到a和b時(shí)候的解呢?
因?yàn)閍 % b = a - (a/b)*b,這里的/是整除計(jì)算的意思,我們代入:
顯然對于a和b來說,它的一組解就是y0和x0 - (a/b)*b*y0,我們把這幾行計(jì)算加在代碼當(dāng)中即可,非常簡單:
def exgcd(a, b, x=1, y=0): # 當(dāng)b=0的時(shí)候return if b == 0: return a, x, y # 遞歸調(diào)用,獲取b, a%b時(shí)的gcd與通項(xiàng)解 gcd, x, y = exgcd(b, a%b, x, y) # 代入,得到新的通項(xiàng)解 x, y = y, x - a//b*y return gcd, x, y這里我建議大家不要死記代碼,都去推導(dǎo)一下遞歸的這個(gè)推導(dǎo)公式。這個(gè)公式搞明白了,即使代碼記不住也沒有關(guān)系,后面臨時(shí)用到的時(shí)候再推導(dǎo)也可以。不然的話,即使背下來了代碼也不記得什么意思,如果碰到的場景稍微變動(dòng)一下,可能還是做不出來。
逆元與解逆元
拓展歐幾里得算法我們理解了,但是好像看不出來它到底有什么用。一般情況下我們也碰不到讓我們計(jì)算通解的情況,但其實(shí)是有用的,用的最多的一個(gè)功能就是計(jì)算逆元。
在解釋逆元之前先來看一個(gè)問題,我們有兩個(gè)數(shù)a和b,和一個(gè)模底數(shù)p。我們可以得到(a + b) % p = (a%p + b%p)%p,也可以得到 (a - b)%p = (a%p - b%p)%p。甚至還可以得到 (a*b)% p =(a%p * b%p) %p,這些都是比較明確的,但是(a / b) % p = (a % p / b % p) % p,這個(gè)式子成立嗎?
最后的式子是不成立的,因?yàn)槟?shù)沒有除法的傳遞性,我們可以很方便舉出反例。比如a是20, b是10,p是4,(a/b)%p=2,而(a %p / b%p) % p = 0。
這就導(dǎo)致了一個(gè)問題,假如說我們在一連串計(jì)算當(dāng)中,由于最終的結(jié)果特別大,我們無法存儲精確的值,希望存儲它關(guān)于一個(gè)模底數(shù)取模之后的結(jié)果。但是我們的計(jì)算當(dāng)中又涉及除法,這個(gè)時(shí)候應(yīng)該怎么辦?
這個(gè)時(shí)候就需要用到逆元了,逆元也叫做數(shù)論倒數(shù)。它其實(shí)起到一個(gè)和倒數(shù)類似的效果,假設(shè)a關(guān)于模底數(shù)p的逆元是x,那么可以得到:ax = 1 (mod p)
所以我們想要算 (a / b) % p,可以先求出b的逆元假設(shè)是inv(b),然后轉(zhuǎn)化成(a%p * inv(b)%p)%p。
這個(gè)逆元顯然不會從天上掉下來,需要我們設(shè)計(jì)算法去求出來,這個(gè)用來求的算法就用到拓展歐幾里得,我們下面來看一下推導(dǎo)過程。
假設(shè)a和b互質(zhì),那么gcd(a, b) = 1,代入:
所以x是a關(guān)于b的逆元,反之可以證明y是b關(guān)于a的逆元。
這么計(jì)算是有前提的,就是a和b互質(zhì),也就是說a和b的最大公約數(shù)為1。否則的話這個(gè)計(jì)算是不成立的,也就是說a沒有逆元。那么整個(gè)求解逆元的過程其實(shí)就是調(diào)用拓展歐幾里得的過程,把問題說清楚花了很多筆墨,但是寫成代碼只有兩三行:
def cal_inv(a, m): gcd, x, y = exgcd(a, m) # 如果gcd不為1,那么說明沒有逆元,返回-1 return (x % m + m) % m if gcd == 1 else -1在return的時(shí)候我們對x的值進(jìn)行了縮放,這是因?yàn)?strong>x有可能得到的是負(fù)數(shù),我們把它縮放到0到m的范圍當(dāng)中。
逆元的求解方法除了拓展歐幾里得之外,還有一種算法,就是利用費(fèi)馬小定理。根據(jù)費(fèi)馬小定理,在m為質(zhì)數(shù)的時(shí)候,可以得到
等式兩邊同時(shí)除以a,也就是乘上a的逆元,可以得到:
也就是說我們求出a^{m-2}然后再對m取模就得到了a的逆元,我們使用快速冪可以很方便地求出來。但是這個(gè)只有m為質(zhì)數(shù)的時(shí)候才可以使用。
總結(jié)
今天我們聊了歐幾里得定理聊了輾轉(zhuǎn)相除法還聊了拓展歐幾里得和求解逆元,雖然這些內(nèi)容單獨(dú)來看并不難,合在一篇文章當(dāng)中量還是不小的。這些算法底層的基礎(chǔ)知識是數(shù)論,對于沒有參加過競賽的同學(xué)來說可能有些陌生,但是它也是算法領(lǐng)域一個(gè)很重要的分支。
如果喜歡本文,可以的話,請點(diǎn)個(gè)關(guān)注,給我一點(diǎn)鼓勵(lì),也方便獲取更多文章。
總結(jié)
以上是生活随笔為你收集整理的扩展欧几里得算法求逆元_从辗转相除法到求逆元,数论算法初体验的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 9、java常用 设计模式
- 下一篇: bigdecimal 小于等于0_图解小