四、數論題集整理
一、數論基本概念
1、整除性
若a和b都爲整數,a整除b是指b是a的倍數,a是b的約數(因數、因子),記爲a|b。整除的大部分性質都是顯而易見的,爲了闡述方便,我給這些性質都隨便起了個名字。
i) 任意性,若a|b,則對於任意非零整數m,有am|bm。
ii) 傳遞性,若a|b,且b|c,則a|c。
iii) 可消性,若a|bc,且a和c互素(互素的概念下文會講到),則a|b。
iv) 組合性,若c|a,且c|b,則對於任意整數m、n,有c|(ma+nb)。
拿一個我還未出生時的初二數學競賽題就能概括整除的性質了。
【例題1】(公元1987年初二數學競賽題) x,y,z均爲整數,若11|(7x+2y-5z),求證:11|(3x-7y+12z)。
非常典型的一個問題,爲了描述方便,令a = (7x+2y-5z),b = (3x-7y+12z),通過構造可以得到一個等式:4a + 3b = 11(3x-2y+3z),則3b = 11(3x-2y+3z) - 4a。
任意性+組合性,得出 11 |(11(3x-2y+3z) - 4a) = 11|3b。
可消性,由於11和3互素,得出 11 | b,證明完畢。
2、素數
a.素數與合數
素數又稱質數,素數首先滿足條件是要大於等於2,並且除了1和它本身外,不能被其它任何自然數整除;其它的數稱爲合數;而1既非素數也非合數。
b.素數判定
如何判定一個數是否爲素數?
i) 對n做[2, n)範圍內的餘數判定(C++中的'%'運算符),如果有至少一個數用n取餘後爲0,則表明n爲合數;如果所有數都不能整除n,則n爲素數,算法複雜度O(n)。
ii) 假設一個數能整除n,即a|n,那麼n/a也必定能整除n,不妨設a <= n/a,則有a^2 <= n,即a <= sqrt(n)(sqrt表示對n開根號),所以在用i)的方法進行取餘的時候,範圍可以縮小到sqrt(n),所以算法複雜度降爲O( sqrt(n) )。
iii) 如果n是合數,那麼它必然有一個小於等於sqrt(n)的素因子,只需要對sqrt(n)內的素數進行測試即可,需要預處理求出sqrt(n)中的素數,假設該範圍內素數的個數爲s,那麼複雜度降爲O(s)。
c.素數定理
當x很大時,小於x的素數的個數近似等於x/ln(x),其中ln(x)表示x的自然對數,用極限表示如圖一-2-1所示:
圖一-2-1
從這個定理可以發現,程序中進行素數判定的時候,用ii)方法和iii)方法差了至少一個數量級。
d.素數篩選法
【例題2】給定n(n < 10000)個數,範圍爲[1, 2^32),判定它是素數還是合數。
首先1不是素數,如果n>1,則枚舉[1,sqrt(n)]範圍內的素數進行試除,如果至少有一個素數能夠整除n,則表明n是合數,否則n是素數。
[1,sqrt(n)]範圍內的素數可以通過篩選法預先篩出來,用一個數組notprime[i]標記i是素數與否,篩選法有很多,這裏介紹一種最常用的篩選法——Eratosthenes篩選法。
直接給出僞代碼:
#define MAXP 65536
#define LL __int64
void Eratosthenes() {
notprime[1] = true;
primes[0] = 0;
for(int i = 2; i < MAXP; i++) {
if( !notprime[i] ) {
primes[ ++primes[0] ] = i;
//需要注意i*i超出整型後變成負數的問題,所以轉化成 __int64
for(LL j = (LL)i*i; j < MAXP; j += i) {
notprime[j] = true;
}
}
}
}
notprime[i]爲真表明i爲合數,否則i爲素數(因爲全局變量初始值爲false,篩選法預處理只做一次,所以不需要初始化)。算法的核心就是不斷將notprime[i]標記爲true的過程,首先從小到大進行枚舉,遇到notprime[i]爲假的,表明i是素數,將i保存到數組primes中,然後將i的倍數都標記爲合數,由於i*2、i*3、i*(i-1)在[1, i)的篩選過程中必定已經被標記爲合數了,所以i的倍數只需要從i*i開始即可,避免不必要的時間開銷。
雖然這個算法有兩個嵌套的輪詢,但是第二個輪詢只有在i是素數的時候纔會執行,而且隨着i的增大,它的倍數會越來越少,所以整個算法的時間複雜度並不是O(n^2),而且遠遠小於O(n^2),在notprime進行賦值的時候加入一個計數器count,計數器的值就是該程序的總執行次數,對MAXP進行不同的值測試發現 int(count / MAXP) 的值隨着MAXP的增長變化非常小,總是維持在2左右,所以這個算法的複雜度可以近似看成是O(n),更加確切的可以說是O(nC),其中C爲常數,C一般取2。
事實上,實際應用中由於空間的限制(空間複雜度爲O(n)),MAXP的值並不會取的很大,10^7基本已經算是極限了,再大的素數測試就需要用到Rabin-Miller
(第三章中會介紹該算法的具體實現)大數判素了。
3、因數分解
a、算術基本定理
算術基本定理可以描述爲:對於每個整數n,都可以唯一分解成素數的乘積,如圖一-3-1所示:
圖一-3-1
這裏的素數並不要求是不一樣的,所以可以將相同的素數進行合併,採用素數冪的乘積進行表示,如圖一-3-2所示:
圖一-3-2
證明方法採用數學歸納法,此處略去。
b、素數拆分
給定一個數n,如何將它拆分成素數的乘積呢?
還是用到上面講到的試除法,假設 n = pm 並且 m>1,其中p爲素數,如果p > sqrt(n),那麼根據算數基本定理,m中必定存在一個小於等於sqrt(n)的素數,所以我們不妨設p <= sqrt(n)。
然後通過枚舉[2, sqrt(n)]的素數,如果能夠找到一個素數p,使得n mod p == 0(mod 表示取餘數、也稱爲模)。於是m = n/p,這時還需要注意一點,因爲m中可能也有p這個素因子,所以如果p|m,需要繼續試除,令m' = m/p,直到將所有的素因子p除盡,統計除的次數e,於是我們得到了 n = (p^e) * n',然後繼續枚舉素數對n'做同樣的試除。
枚舉完[2, sqrt(n)]的素數後,得到表達式如圖一-3-3所示:
圖一-3-3
這時有兩種情況:
i) S == 1,則素數分解完畢;
ii) S > 1, 根據算術基本定理,S 必定爲素數,而且是大於sqrt(n)的素數,並且最多隻有1個,這種情況同樣適用於n本身就是素數的情況,這時n = S。
這樣的分解方式稱爲因數分解,各個素因子可以用一個二元的結構體來存儲。算法時間複雜度爲O( s ),s爲sqrt(n)內素數的個數。
c、因子個數
樸素的求因子個數的方法爲枚舉[1, n]的數進行餘數判定,複雜度爲O(n),這裏加入一個小優化,如果m爲n的因子,那麼必然n/m也爲n的因子,不妨設m <= n/m,則有m <= sqrt(n),所以只要枚舉從[1, sqrt(n)]的因子然後計數即可,複雜度變爲O(sqrt(n))。
【例題3】給定X,Y(X, Y < 2^31),求X^Y的因子數 mod 10007。
由於這裏的X^Y已經是天文數字,利用上述的枚舉法已經無法滿足要求,所以我們需要換個思路。考慮到任何整數都能表示成素數的乘積,那麼X^Y也不例外,我們首先將X進行因數分解,那麼X^Y可以表示成圖一-3-4所示的形式:
圖一-3-4
容易發現X^Y的因子一定是p1、p2、...、pk的組合,並且p1可以取的個數爲[0, Ye1],p2可以取的個數爲[0, Ye2],pk可以取的個數爲[0, Yek],所以根據乘法原理,總的因子個數就是這些指數+1的連乘,即(1 + Ye1) * (1 + Ye2) * ... * (1 + Yek)。
通過這個問題,可以得到更加一般的求因子個數的公式,如果用ei表示X分解素因子之後的指數,那麼X的因子個數就是(1 + e1) * (1 + e2) * ... * (1 + ek)。
d、因子和
【例題4】給定X,Y(X, Y < 2^31),求X^Y的所有因子之和 mod 10007。
同樣還是將X^Y表示成圖一-3-4的形式,然後就變成了標準素數分解後的數的因子和問題了。考慮數n,令n的因子和爲s(n),對n進行素數分解後的,假設最小素數爲p,素因子p的個數爲e,那麼n = (p^e)n'。
容易得知當n的因子中p的個數爲0時,因子之和爲s(n')。更加一般地,當n的因子中p的個數爲k的時候,因子之和爲(p^k)*s(n'),所以n的所有因子之和就可以表示成:
s(n) = (1 + p^1 + p^2 + ... p^e) * s(n') = (p^(e+1) - 1) / (p-1) * s(n')
s(n')可以通過相同方法遞歸計算。最後可以表示成一系列等比數列和的乘積。
令g(p, e) = (p^(e+1) - 1) / (p-1),則s(n) = g(p1, e1) * g(p2, e2) * ... * g(pk, ek)。
4、最大公約數(GCD)和最小公倍數(LCM)
兩個數a和b的最大公約數(Greatest Common Divisor)是指同時整除a和b的最大因數,記爲gcd(a, b)。特殊的,當gcd(a, b) = 1,我們稱a和b互素(上文談到整除的時候略有提及)。
兩個數a和b的最小公倍數(Leatest Common Multiple)是指同時被a和b整除的最小倍數,記爲lcm(a, b)。特殊的,當a和b互素時,lcm(a, b) = ab。
gcd是基礎數論中非常重要的概念,求解gcd一般採用輾轉相除法(這個方法會在第二章開頭着重介紹,這裏先引出概念),而求lcm需要先求gcd,然後通過lcm(a, b) = ab / gcd(a, b)求解。
這裏無意中引出了一個恆等式:lcm(a, b) * gcd(a, b) = ab。這個等式可以通過算術基本定理進行證明,證明過程可以通過圖一-4-1秒懂。
圖一-4-1
需要說明的是這裏的a和b的分解式中的指數是可以爲0的,也就是說p1是a和b中某一個數的最小素因子,p2是次小的素因子。lcm(a, b)和gcd(a, b)相乘,相當於等式右邊的每個素因子的指數相加,即min{xi, yi} + max{xi, yi} = xi + yi,正好對應了a和b的第i個素數分量的指數之和,得證。
給這樣的gcd和lcm表示法冠個名以便後續使用——指數最值表示法。
【例題5】三個未知數x, y, z,它們的gcd爲G,lcm爲L,G和L已知,求(x, y, z)三元組的個數。
三個數的gcd可以參照兩個數gcd的指數最值表示法,只不過每個素因子的指數上是三個數的最值(即min{x1, y1, z1}),那麼這個問題首先要做的就是將G和L分別進行素因子分解,然後輪詢L的每個素因子,對於每個素因子單獨處理。
假設素因子爲p,L分解式中p的指數爲l,G分解式中p的指數爲g,那麼顯然l < g時不可能存在滿足條件的三元組,所以只需要討論l >= g的情況,對於單個p因子,問題轉化成了求三個數x1, y1, z1,滿足min{x1, y1, z1} = g且max{x1, y1, z1} = l,更加通俗的意思就是三個數中最小的數是g,最大的數是l,另一個數在[g, l]範圍內,這是一個排列組合問題,三元組{x1,
y1, z1}的種類數當l == g時只有1中,否則答案就是 6(l - g)。
最後根據乘法原理將每個素因子對應的種類數相乘就是最後的答案了。
5、同餘
a、模運算
給定一個正整數p,任意一個整數n,一定存在等式n = kp + r; 其中k、r是整數,且滿足0 <= r < p,稱k爲n除以p的商, r爲n除以p的餘數,表示成n % p = r (這裏採用C++語法,%表示取模運算)。
對於正整數和整數a, b, 定義如下運算:
取模運算:a % p(a mod p),表示a除以p的餘數。
模p加法:(a + b) % p = (a%p + b%p) % p
模p減法:(a - b) % p = (a%p - b%p) % p
模p乘法:(a * b) % p = ((a % p)*(b % p)) % p
冪模p : (a^b) % p = ((a % p)^b) % p
模運算滿足結合律、交換律和分配律。
a≡b (mod n) 表示a和b模n同餘,即a和b除以n的餘數相等。
【例題6】一個n位十進制數(n <= 1000000)必定包含1、2、3、4四個數字,現在將它順序重排,求給出一種方案,使得重排後的數是7的倍數。
取出1、2、3、4後,將剩下的數字隨便排列得到一個數a,令剩下的四個數字排列出來的數爲b,那麼就是要找到一種方案使得(a*10000 + b) % 7等於0。
但是a真的可以隨便排嗎?也就是說如果無論a等於多少,都能找到這樣的b滿足等式成立,那麼a就可以隨便排。
我們將等式簡化:
(a*10000 + b) % 7 = (a*10000%7 + b%7) % 7
令 k = a*10000%7 = a*4%7,容易發現k的取值爲[0, 7),如果b%7的取值也是[0, 7),那這個問題就可以完美解決了,很幸運的是,的確可以構造出7個這樣的b。具體參見下圖:
圖一-5-1
b、快速冪取模
冪取模常常用在RSA加密算法的加密和解密過程中,是指給定整數a,正整數n,以及非零整數p,求a^n % p。利用模p乘法,這個問題可以遞歸求解,即令f(n) = a^n%p,那麼f(n-1) = a^(n-1)%p,f(n) = a*f(n-1) % p,這樣就轉化成了遞歸式。但是遞歸求解的時間複雜度爲O(n),往往當n很大的時候就很難在規定時間內出解了。
當n爲偶數時,我們可以將a^n%p拆成兩部分,令b = a^(n/2)%p,則a^n%p = b*b%p;
當n爲奇數時,可以拆成三部分,令b = a^(n/2)%p,則a^n%p = a*b*b%p;
上述兩個等式中的b可以通過遞歸計算,由於每次都是除2,所以時間複雜度是O(logn)。
c、循環節
【例題7】f[1] = a, f[2] = b, f[3] = c, 當n>3時 f[n] = (A*f[n-1] + B*f[n-2] + C*f[n-3]) % 53,給定a, b, c, A, B, C,求f[n] (n < 2^31)。
由於n非常大,循環模擬求解肯定是不現實的,仔細觀察可以發現當n>3時,f[n]的值域爲[0, 53),並且連續三個數f[n-1]、f[n-2]、f[n-3]一旦確定,那麼f[n]也就確定了,而f[n-1]、f[n-2]、f[n-3]這三個數的組合數爲53*53*53種情況,那麼對於一個下標k<n,假設f[k]已經求出,並且滿足f[k-1] == f[n-1]且f[k-2] == f[n-2]且f[k-3]
== f[n-3], 則f[n]必定等於f[k],這裏的f[k...n-1]就被稱爲這個數列的循環節。
並且在53*53*53次計算之內必定能夠找到循環節,這個是顯而易見的。
二、數論基礎知識
1、歐幾里德定理(輾轉相除法)
定理:gcd(a, b) = gcd(b, a % b)。
證明:a = kb + r = kb + a%b,則a % b = a - kb。令d爲a和b的公約數,則d|a且d|b 根據整除的組合性原則,有d|(a-kb),即d|(a%b)。
這就說明如果d是a和b的公約數,那麼d也一定是b和a%b的公約數,即兩者的公約數是一樣的,所以最大公約數也必定相等。
這個定理可以直接用遞歸實現,代碼如下:
int gcd(int a, int b) {
return b ? gcd(b, a%b) : a;
}
這個函數揭示了一個約定俗成的概念,即任何非零整數和零的最大公約數爲它本身。
【例題8】f[0] = 0, 當n>1時,f[n] = (f[n-1]+a) % b,給定a和b,問是否存在一個自然數k (0 <= k< b),是f[n]永遠都取不到的。
永遠有多遠?並不是本題的範疇。
但是可以發現的是這裏的f[...]一定是有循環節的,如果在某個循環節內都無法找到那個自然數k,那麼必定是永遠都找不到了。
求出f[n]的通項公式,爲f[n] = an % b,令an = kb + r,那麼這裏的r = f[n],如果t = gcd(a, b),r = an-kb = t ( (a/t)n - (b/t)k ),則有t|r,要滿足所有的r使得t|r,只有當t = 1的時候,於是這個問題的解也就出來了,只要求a和b的gcd,如果gcd(a, b) > 1,則存在一個k使得f[n]永遠都取不到,直觀的理解是當gcd(a,
b) > 1,那麼f[n]不可能是素數。
2、擴展歐幾里德定理
a、線性同餘
線性同餘方程(也可以叫模線性方程)是最基本的同餘方程,即ax≡b (mod n),其中a、b、n都爲常量,x是未知數,這個方程可以進行一定的轉化,得到:ax = kn + b,這裏的k爲任意整數,於是我們可以得到更加一般的形式即:ax + by + c = 0,這個方程就是二維空間中的直線方程,但是x和y的取值爲整數,所以這個方程的解是一些排列成直線的點集。
b、同餘方程求解
求解同餘方程第一步是轉化成一般式:ax + by = c,這個方程的求解步驟如下:
i) 首先求出a和b的最大公約數d = gcd(a, b),那麼原方程可以轉化成d(ax/d + by/d) = c,容易知道(ax/d + by/d)爲整數,如若d不能整除b,方程必然無解,算法結束;否則進入ii)。
ii) 由i)可以得知,方程有解則一定可以表示成 ax + by = c = gcd(a, b)*c',那麼我們先來看如何求解d = gcd(a, b) = ax + by,根據歐幾里德定理,有:
d = gcd(a, b) = gcd(b, a%b) = bx' + (a%b)y' = bx' + [a-b*(a/b)]y' = ay' + b[x' - (a/b)y']
於是有x = y', y = x' - (a/b)y'。
由於gcd(a, b)是一個遞歸的計算,所以在求解(x, y)時,(x', y')其實已經利用遞歸計算出來了,遞歸出口爲b == 0的時候(對比輾轉相除,也是b == 0的時候遞歸結束),那麼這時方程的解x0 = 1, y0 = 0。代碼如下:
#define LL __int64
LL Extend_Euclid(LL a, LL b, LL &X, LL &Y) {
LL q, temp;
if( !b ) {
X = 1; Y = 0;
return a;
}else {
q = Extend_Euclid(b, a % b, X, Y);
temp = X;
X = Y;
Y = temp - (a / b) * Y;
return q;
}
}
擴展歐幾里德算法和歐幾里德算法的返回值一致,都是gcd(a, b),傳參多了兩個未知數X, Y,採用引用的形式進行傳遞,對應上文提到的x, y,遞歸出口爲b == 0,這時返回值爲當前的a,因爲gcd(a, 0) = a,(X, Y)初值爲(1, 0),然後經過回溯不斷計算新的(X, Y),這個計算是利用了之前的(X, Y)進行迭代計算的,直到回溯到最上層算法終止。最後得到的(X, Y)就是方程gcd(a,
b) = ax + by的解。
通過擴展歐幾里德求的是ax + by = gcd(a, b)的解,令解爲(x0, y0),代入原方程,得:ax0 + by0 = gcd(a, b),如果要求ax + by = c = gcd(a, b)*c',可以將上式代入,得:ax + by = c = (ax0 + by0)c',則x = x0c', y = y0c',這裏的(x, y)只是這個方程的其中一組解,x的通解爲 {
x0c' + kb/gcd(a, b) | k爲任意整數 },y的通解可以通過x通解的代入得出。
【例題9】有兩隻青蛙,青蛙A和青蛙B,它們在一個首尾相接的數軸上。設青蛙A的出發點座標是x,青蛙B的出發點座標是y。青蛙A一次能跳m米,青蛙B一次能跳n米,兩隻青蛙跳一次所花費的時間相同。數軸總長L米。要求它們至少跳了幾次以後纔會碰面。
假設跳了t次後相遇,則可以列出方程:(x + mt) % L = (y + nt) % L
將未知數t移到等式左邊,常數移到等式右邊,得到模線性方程:(m-n)t%L = (y-x)%L (即 ax≡b (mod n) 的形式)
利用擴展歐幾里德定理可以求得t的通解{ t0 + kd | k爲任意整數 },由於這裏需要求t的最小正整數,而t0不一定是最小的正整數,甚至有可能是負數,我們發現t的通解是關於d同餘的,所以最後的解可以做如下處理:ans = (t0 % d + d) % d。
c、逆元
模逆元的最通俗含義可以效仿乘法,a*x = 1,則稱x爲a在乘法域上的逆(倒數);同樣,如果ax≡1 (mod n),則稱b爲a模n的逆,簡稱逆元。求a模n的逆元,就是模線性方程ax≡b (mod n)中b等於1的特殊形式,可以用擴展歐幾里德求解。並且在gcd(a, n) > 1時逆不存在。
3、中國剩餘定理
上文提到了模線性方程的求解,再來介紹一種模線性方程組的求解,模線性方程組如圖二-3-1所示,其中(ai, mi)都是已知量,求最小的x滿足以下n個等式:
圖二-3-1
將模數保存在mod數組中,餘數保存在rem數組中,則上面的問題可以表示成以下幾個式子,我們的目的是要求出一個最小的正整數K滿足所有等式:
K = mod[0] * x[0] + rem[0] (0)
K = mod[1] * x[1] + rem[1] (1)
K = mod[2] * x[2] + rem[2] (2)
K = mod[3] * x[3] + rem[3] (3)
... ...
這裏給出我的算法,大體的思想就是每次合併兩個方程,經過n-1次合併後剩下一個方程,方程的自變量取0時得到最小正整數解。算法描述如下:
i) 迭代器i = 0
ii) x[i] = (newMod[i]*k + newRem[i]) (k爲任意整數)
iii) 合併(i)和(i+1),得 mod[i] * x[i] - mod[i+1] * x[i+1] = rem[i+1] - rem[i]
將x[i]代入上式,有 newMod[i]*mod[i]*k - mod[i+1] * x[i+1] = rem[i+1] - rem[i] - newRem[i]*mod[i]
iv) 那麼產生了一個形如 a*k + b*x[i+1] = c的同餘方程,
其中a = newMod[i]*mod[i], b = - mod[i+1], c = rem[i+1] - rem[i] - newRem[i]*mod[i]
求解同餘方程,如果a和b的gcd不能整除c,則整個同餘方程組無解,算法結束;
否則,利用擴展歐幾里德求解x[i+1]的通解,通解可以表示成 x[i+1] = (newMod[i+1]*k + newRem[i+1]) (k爲任意整數)
v) 迭代器i++,如果i == n算法結束,最後答案爲 newRem[n-1] * mod[n-1] + rem[n-1];否則跳轉到ii)繼續迭代計算。
4、歐拉函數
a、互素
兩個數a和b互素的定義爲:gcd(a, b) = 1,那麼如何求不大於n且與n互素的數的個數呢?
樸素算法,枚舉i從1到n,當gcd(i, n)=1時計數器++,算法時間複雜度O(n)。
這裏引入一個新的概念:用φ(n)表示不大於n且與n互素的數的個數,該函數以歐拉的名字命名,稱爲歐拉函數。
如果n是一個素數,即n = p,那麼φ(n) = p-1(所有小於n的都互素);
如果n是素數的k次冪,即n = p^k,那麼φ(n) = p^k - p^(k-1) (除了p的倍數其它都互素);
如果m和n互素,那麼φ(mn) = φ(m)φ(n)(可以利用上面兩個性質進行推導)。
將n分解成如圖二-4-1的素因子形式,那麼利用上面的定理可得φ(n)如圖二-4-2所示:
圖二-4-1
圖二-4-2
前面已經講到n的因子分解複雜度爲O(k),所以歐拉函數的求解就是O(k)。
b、篩選法求解歐拉函數
由於歐拉函數的表示法和整數的素數拆分表示法很類似,都可以表示成一些素數的函數的乘積,所以同樣可以利用篩選法進行求解。僞代碼如下:
#define MAXP 2000010
#define LL __int64
void Eratosthenes_Phi() {
notprime[1] = true;
for(int i = 1; i < MAXP; i++) phi[i] = 1;
for(int i = 2; i < MAXP; i++) {
if( !notprime[i] ) {
phi[i] *= i - 1;
// 和傳統素數篩法的區別在於這個i+i
for(int j = i+i; j < MAXP; j += i) {
notprime[j] = true;
int n = j / i;
phi[j] *= (i - 1);
while(n % i == 0) n /= i, phi[j] *= i;
}
}
}
}
這裏的phi[i]保存了i這個數的歐拉函數,還是利用素數篩選將所有素數篩選出來,然後針對每個素因子計算它的倍數含有該素因子的個數,利用歐拉公式計算該素因子帶來的歐拉函數分量,整個篩選過程可以參考素數篩選。
c、歐拉定理和費馬小定理
歐拉定理:若n,a爲正整數,且n,a互素,則: 。
費馬小定理:若p爲素數,a爲正整數且和p互素,則: 。
由於當n爲素數時φ(n) = p-1,可見費馬小定理是歐拉定理的特殊形式。
證明隨處可見,這裏講一下應用。
【例題10】整數a和n互素,求a的k次冪模n,其中k = X^Y, 正整數a,n,X,Y(X,Y<=10^9)爲給定值。
問題要求的是a^(X^Y) % n,指數上還是存在指數,需要將指數化簡,注意到a和n互素,所以可以利用歐拉定理,令X^Y = kφ(n) + r,那麼kφ(n)部分並不需要考慮,問題轉化成求r = X^Y % φ(n),可以採用快速冪取模,二分求解,得到r後再採用快速冪取模求解a^r % n。
5、容斥原理
容斥原理是應用在集合上的,來看圖二-5-1,要求圖中兩個圓的並面積,我們的做法是先將兩個圓的面積相加,然後發現相交的部分多加了一次,予以減去;對於圖二-5-2的三個圓的並面積,則是先將三個圓的面積相加,然後減去兩兩相交的部分,而三個圓相交的部分被多減了一次,予以加回。
圖二-5-1
圖二-5-2
這裏的“加”就是“容”,“減”就是“斥”,並且“容”和“斥”總是交替進行的(一個的加上,兩個的減去,三個的加上,四個的減去),而且可以推廣到n個元素的情況。
【例題11】求小於等於m(m < 2^31)並且與n(n < 2^31)互素的數的個數。
當m等於n,就是一個簡單的歐拉函數求解。
但是一般情況m都是不等於n的,所以可以直接擯棄歐拉函數的思路了。
考慮將n分解成素數冪的乘積,來看一種最簡單的情況,當n爲素數的冪即n = p^k時,顯然答案等於m - m/p(m/p表示的是p的倍數,去掉p的倍數,則都是和n互素的數了);然後再來討論n是兩個素數的冪的乘積的情況,即n = p1^k1 * p2^k2,那麼我們需要做的就是找到p1的倍數和p2的倍數,並且要減去p1和p2的公公倍數,這個思想其實已經是容斥了,所以這種情況下答案爲:m -
( m/p1 + m/p2 - m/(p1*p2) )。
類比兩個素因子,如果n分解成s個素因子,也同樣可以用容斥原理求解。
容斥原理其實是枚舉子集的過程,常見的枚舉方法爲dfs,也可以採用二進制法(0表示取,1表示不取)。這裏給出一版dfs版本的容斥原理的僞代碼,用於求解小於等於m且與n互素的數的個數。
#define LL __int64
void IncludeExclude(int depth, LL m, LL mul, int op, int* p, LL &ans) {
if(m < mul) return ;
if(depth == p[0]) {
ans += (op ? -1 : 1) * (m / mul);
return ;
}
for(int i = 0; i < 2; i++) {
// 0 表示不取, 1表示取
IncludeExclude( depth+1, m, mul * (i?p[depth+1]:1), op^i, p, ans );
}
}
p[ 1 : p[0] ]存儲的是n的所有素因子,p[0]表示數組長度,mul表示該次的素因子子集的乘積,op表示子集的奇偶性,ans存儲最後的答案。
例如求[1, 9]中和6互素的數的個數,這時p = [2, 2, 3] (注意p[0]是存素數的個數的,6分解的素因子爲2和3)。
ans = 9/1 - (9/2 + 9/3) + 9/6 = 3,ans分爲三部分,0個數的組合,1個數的組合,2個數的組合。
三、數論常用算法
1、Rabin-Miller 大素數判定
對於一個很大的數n(例如十進制表示有100位),如果還是採用試除法進行判定,時間複雜度必定難以承受,目前比較穩定的大素數判定法是拉賓-米勒(Rabin-Miller)素數判定。
拉賓-米勒判定是基於費馬小定理的,即如果一個數p爲素數的條件是對於所有和p互素的正整數a滿足以下等式:。
然而我們不可能試遍所有和p互素的正整數,這樣的話和試除比算法的複雜度反而更高,事實上我們只需要取比p小的幾個素數進行測試就行了。
具體判斷n是否爲素數的算法如下:
i) 如果n==2,返回true;如果 n<2|| !(n&1), 返回false;否則跳到ii)。
ii) 令n = m*(2^k) + 1,其中m爲奇數,則n-1 = m*(2^k)。
iii) 枚舉小於n的素數p(至多枚舉10個),對每個素數執行費馬測試,費馬測試如下:計算pre = p^m % n,如果pre等於1,則該測試失效,繼續回到iii)測試下一個素數;否則進行k次計算next = pre^2 % n,如果next == 1 && pre != 1 && pre != n-1則n必定是合數,直接返回;k次計算結束判斷pre的值,如果不等於1,必定是合數。
iv) 10次判定完畢,如果n都沒有檢測出是合數,那麼n爲素數。
僞代碼如下:
bool Rabin_Miller(LL n) {
LL k = 0, m = n-1;
if(n == 2) return true;
if(n < 2 || !(n & 1)) return false;
// 將n-1表示成m*2^k
while( !(m & 1) ) k++, m >>= 1;
for(int i = 0; i < 10; i++) {
if(p[i] == n)
return true;
if( isRealComposite(p[i], n, m, k) ) {
return false;
}
}
return true;
}
這裏的函數isRealComposite(p, n, m, k)就是費馬測試,p^(m*2^k) % n不等於1則n必定爲合數,這是根據費馬小定理得出的(注意)。n-1 = m*(2^k)
isRealComposite實現如下:
bool isRealComposite(LL p, LL n, LL m, LL k) {
LL pre = Power_Mod(p, m, n);
if(pre == 1) {
return false;
}
while(k--) {
LL next = Product_Mod(pre, pre, n);
if(next == 1 && pre != 1 && pre != n-1)
return true;
pre = next;
}
return ( pre != 1 );
}
這裏Power_Mod(a, b, n)即a^b%n,Product_Mod(a, b, n)即a*b%n,而k次測試的基於費馬小定理的一個推論:x^2 % n = 1當n爲素數時x的解只有兩個,即1和n-1。
2、Pollard-rho 大數因式分解
有了大數判素,就會伴隨着大數的因式分解,Pollard-rho是一個大數分解的隨機算法,能夠在O(n ^(1/4) )的時間內找到n的一個素因子p,然後再遞歸計算n' = n/p,直到n爲素數爲止,通過這樣的方法將n進行素因子分解。
Pollard-rho的策略爲:從[2, n)中隨機選取k個數x1、x2、x3、...、xk,求任意兩個數xi、xj的差和n的最大公約數,即d = gcd(xi - xj, n),如果1 < d < n,則d爲n的一個因子,直接返回d即可。
然後來看如何選取這k個數,我們採用生成函數法,令x1 = rand()%(n-1) + 1,xi = (xi-1 ^ 2 + 1 ) mod n,很明顯,這個序列是有循環節的,就像圖三-2-1那樣。
圖三-2-1
我們需要做的就是在它進入循環的時候及時跳出循環,因爲x1是隨機選的,x1選的不好可能使得這個算法永遠都找不到n的一個範圍在(1, n)的因子,這裏採用歩進法,保證在進入環的時候直接跳出循環,具體算法僞代碼如下:
LL Pollard_rho(LL n) {
LL x = rand() % (n - 1) + 1;
LL y = x;
LL i = 1, k = 2;
do {
i++;
LL p = gcd(n + y - x, n); // 這裏傳入的gcd需要是正數
if(1 < p && p < n) {
return p;
}
if(i == k) {
k <<= 1;
y = x;
}
x = Func(x, n);
}while(x != y);
return n;
}
3、RSA原理
RSA算法有三個參數,n、pub、pri,其中n等於兩個大素數p和q的乘積(n = p*q),pub可以任意取,但是要求與(p-1)*(q-1)互素,pub*pri % () = 1 (可以理解爲pri是pub的逆元),那麼這裏的(n, pub)稱爲公鑰,(n, pri)稱爲私鑰。(p-1)*(q-1)
RSA算法的加密和解密是一致的,令x爲明文,y爲密文,則:
加密:y = x ^ pub % n (利用公鑰加密,y = encode(x) )
解密:x = y ^ pri % n (利用私鑰解密,x = decode(y) )
那麼我們來看看這個算法是如何運作的。
假設你得到了一個密文y,並且手上只有公鑰,如何得到明文x,從decode的情況來看,只要知道私鑰貌似就可以了,而私鑰的獲取方式只有一個,就是求公鑰對(p-1)*(q-1)的逆元,如果(p-1)*(q-1)已知,那麼可以利用擴展歐幾里德定理進行求解,問題是(p-1)*(q-1)是未知的,但是我們有n = p*q,於是問題歸根結底其實是難在了對n進行素因子分解上了,Pollard-rho的分解算法時間
複雜度只能達到O(n ^(1/4) ),對int64範圍內的整數可以在幾十毫秒內出解,而當n是幾百位的大數的時候計算時間就只能用天來衡量了。
四、數論題集整理
1、素數和因數分解
2、GCD && LCM
3、同餘性質 和 循環節
4、模線性方程和逆元
5、模線性方程組
6、歐拉函數、歐拉定理、費馬小定理
6、容斥原理
7、大素數判定
8、離散對數-Baby Step Gaint Step算法
9、其它
人一我百!人十我萬!永不放棄~~~懷着自信的心,去追逐夢想。