基礎數論復習


這個復習沒有什么順序的... 想到什么復習什么而已qwq

Miller-Rabin 質數測試

問題描述

測試一個正整數 \(p\) 是否為素數,需要較快且准確的測試方法。\((p \le 10^{18})\)

算法解決

費馬小定理

首先需要了解 費馬小定理

費馬小定理:對於質數 \(p\) 和任意整數 \(a\) ,有 \(a^p \equiv a \pmod p\)

反之,若滿足 \(a^p \equiv a \pmod p\)\(p\) 也有很大概率為質數。

將兩邊同時約去一個 \(a\) ,則有 \(a^{p-1} \equiv 1 \pmod p\)

也即是說:假設我們要測試 \(n\) 是否為質數。我們可以隨機選取一個數 \(a\) ,然后計算 \(a^{n-1} \pmod n\) ,如果結果不為 \(1\) ,我們可以 \(100\%\) 斷定 \(n\) 不是質數。否則我們再隨機選取一個新的數 \(a\) 進行測試。如此反復多次,如果每次結果都是 \(1\) ,我們就假定 \(n\) 是質數。

二次探測定理

該測試被稱為 \(Fermat\) 測試\(Miller\)\(Rabin\)\(Fermat\) 測試上,建立了 \(Miller-Rabin\) 質數測試算法。與 \(Fermat\) 測試相比,增加了一個 二次探測定理

二次探測定理:如果 \(p\) 是奇素數,則 \(x^2 \equiv 1 \pmod p\) 的解為 \(x \equiv 1\)\(x \equiv p - 1 \pmod p\)

這是很容易證明的:

\[\begin{align} x^2 \equiv 1 \pmod p \\ x^2 -1 \equiv 0 \pmod p \\ (x-1) (x+1) \equiv 0 \pmod p \end{align} \]

\(\because\) \(p\) 為奇素數,有且僅有 \(1,p\) 兩個因子。

\(\therefore\) 只有兩解 \(x \equiv 1\)\(x \equiv p - 1 \pmod p\)

如果 \(a^{n-1} \equiv 1 \pmod n\) 成立,\(Miller-Rabin\) 算法不是立即找另一個 \(a\) 進行測試,而是看 \(n-1\) 是不是偶數。如果 \(n-1\) 是偶數,另 \(\displaystyle u=\frac{n-1}{2}\) ,並檢查是否滿足二次探測定理即 \(a^u \equiv 1\)\(a^u \equiv n - 1 \pmod n\)

假設 \(n=341\) ,我們選取的 \(a=2\) 。則第一次測試時,\(2^{340} \bmod 341 = 1\) 。由於 \(340\) 是偶數,因此我們檢查 \(2^{170}\) ,得到 \(2^{170} \bmod 341=1\) ,滿足二次探測定理。同時由於 \(170\) 還是偶數,因此我們進一步檢查\(2^{85} \bmod 341=32\) 。此時不滿足二次探測定理,因此可以判定 \(341\) 不為質數。

將這兩條定理合起來,也就是最常見的 \(Miller-Rabin\) 測試

單次測試失敗的幾率為 \(\displaystyle \frac{1}{4}\) 。那么假設我們做了 \(times\) 次測試,那么我們總錯誤的幾率為 \(\displaystyle (\frac{1}{4})^{times}\) 如果 \(times\)\(20\) 次,那么錯誤概率約為 \(0.00000000000090949470\) 也就是意味着不可能發生的事件。

單次時間復雜度是 \(O(\log n)\) ,總復雜度就是 \(O(times \log n)\) 了。注意 long long 會爆,用 __int128 就行了。

代碼實現

inline bool Miller_Rabin(ll p) {
	if (p <= 2) return p == 2;
	if (!(p & 1)) return false;
	ll u = p - 1; int power = 0;
	for (; !(u & 1); u >>= 1) ++ power;
	For (i, 1, times) {
		ll a = randint(2, p - 1), x = fpm(a, u, p), y;
		for (int i = 1; i <= power; ++ i, x = y) {
			if ((y = mul(x, x, p)) == 1 && x != 1 && x != p - 1) return false;
		}
		if (x != 1) return false;
	}
	return true;
}

Pollard-Rho 大合數分解

問題描述

分解一個合數 \(n\) ,時間效率要求較高。(比試除法 \(O(\sqrt n)\) 要快許多)

算法解決

生日悖論

在一個班級里,假設有 \(60\) 人,所有人生日不同概率是多少?

依次按人考慮,第一個人有 \(1\) 的概率,第二個人要與第一個人不同就是 \(\displaystyle 1 - \frac{1}{365}\) ,第三個人與前兩人不同,那么就是 \(\displaystyle 1 - \frac{2}{365}\) 。那么第 \(i\) 個人就是 \(\displaystyle 1 - \frac{i-1}{365}\)

那么概率就是

\[\displaystyle \prod_{i=1}^{n} (1 - \frac{i-1}{365}) = \prod _{i=0}^{n-1}\frac{365-i}{365}=\frac{365^{\underline{n}}}{365^n} \]

假設我們帶入 \(n\) 等於 \(60\) ,那么這個概率就是 \(0.0058\) ,也就是說基本上不可能發生。

好像是因為和大眾的常識有些違背,所以稱作生日悖論。

假設一年有 \(N\) 天,那么只要 \(n \ge \sqrt N\) 存在相同生日的概率就已經大於 \(50 \%\) 了。

利用偽隨機數判斷

本段參考了 Doggu 大佬的博客

我們考慮利用之前那個性質來判斷,生成了許多隨機數,然后兩兩枚舉判斷。

假設枚舉的兩個數為 \(i, j\) ,假設 \(i > j\) ,那么判斷一下 \(\gcd(i - j, n)\) 是多少,如果非 \(1\) ,那么我們就已經找出了一個 \(n\) 的因子了,這樣的概率隨着枚舉的組數變多會越來越快。

如果我們一開始把這些用來判斷的數都造出來這樣,這樣就很坑了。不僅內存有點惡心,而且其實效率也不夠高。

考慮優化,我們用一個偽隨機數去判斷,不斷生成兩個隨機數,然后像流水線一樣逐個判斷就行了。

有一個隨機函數似乎很優秀,大部分人都用的這個:

\[f(x)=(x^2+a) \bmod n \]

\(a\) 在單次判斷中是一個常數。

然后一般這種簡單的隨機數都會出現循環節,我們判斷一下循環節就行了。

但為了節省內存,需要接下來這種算法。

Floyd 判圈法

兩個人同時在一個環形跑道上起跑,一個人是另外一個人的兩倍速度,那么這個人絕對會追上另外一個人。追上時,意味着其中一個人已經跑了一圈了。

然后就可以用這個算法把退出條件變松,只要有環,我們就直接退出就行了。

如果這次失敗了,那么繼續找另外一個 \(a\) 就行了。

用 Miller-Rabin 優化

這個算法對於因子多,因子值小的數 \(n\) 是較為優異的。

也就是對於它的一個小因子 \(p\)\(Pollard-Rho\) 期望在 \(O(\sqrt p)\) 時間內找出來。

但對於 \(n\) 是一個質數的情況,就沒有什么較好的方法快速判斷了,那么復雜度就是 \(O(\sqrt n)\) 的。

此時就退化成暴力試除法了。

此時我們考慮用前面學習的 \(Miller-Rabin\) 算法來優化這個過程就行了。

此時把這兩個算法結合在一起,就能解決這道題啦qwq

代碼實現

我似乎要特判因子為 \(2\) 的數,那個似乎一直在里面死循環qwq

namespace Pollard_Rho {
	ll c, Mod;

	ll f(ll x) { return (mul(x, x, Mod) + c) % Mod; }

	ll find(ll x) {
		if (!(x & 1)) return 2; Mod = x;
		ll a = randint(2, x - 1), b = a; c = randint(2, x - 1);
		do {
			a = f(a); b = f(f(b));
			ll p = __gcd(abs(a - b), x); 
			if (p > 1) return p;
		} while (b != a);
		return find(x);
	}

	void ReSolve(ll x, vector<ll> &factor) {
		if (x <= 1) return;
		if (Miller_Rabin(x)) { factor.pb(x); return; }
		ll fac = find(x); ReSolve(fac, factor); ReSolve(x / fac, factor);
	}
};

約瑟夫問題

問題描述

首先 \(n\) 個候選人圍成一個圈,依次編號為 \(0\dots n-1\) 。然后隨機抽選一個數 \(k\) ,並 \(0\) 號候選人開始按從 \(1\)\(k\) 的順序依次報數,\(n-1\)號候選人報數之后,又再次從 \(0\) 開始。當有人報到 \(k\) 時,這個人被淘汰,從圈里出去。下一個人從 \(1\) 開始重新報數。

現在詢問:最后一個被淘汰在哪個位置。 \((n \le 10^9, k \le 1000)\)

算法解決

暴力模擬

最直觀的解法是用循環鏈表模擬報數、淘汰的過程,復雜度是 \(O(nk)\)

遞推一

\(f[n]\) 表示當有 \(n\) 個候選人時,最后當選者的編號。 (注意是有 \(n\) 個人,而不是 \(n\) 輪)

\[\begin{cases} f[1] = 0\\ f[n] = (f[n - 1] + k) \pmod n &n > 1 \end{cases} \]

我們考慮用數學歸納法證明:

  1. \[f[1]=0 \]

    顯然當只有 \(1\) 個候選人時,該候選人就是當選者,並且他的編號為 \(0\)

  2. \[f[n] = (f[n - 1] + k) \pmod n \]

    假設我們已經求解出了 \(f[n - 1]\) ,並且保證 \(f[n - 1]\) 的值是正確的。

    現在先將 \(n\) 個人按照編號進行排序:

    0 1 2 3 ... n-1
    

    那么第一次被淘汰的人編號一定是 \(k-1\) (假設 \(k < n\) ,若 \(k > n\) 則為 \((k-1) \bmod n\) )。將被選中的人標記為 \(\underline{\#}\)

    0 1 2 3 ... k-2 # k k+1 k+2 ... n-1
    

    第二輪報數時,起點為 \(k\) 這個候選人。並且只剩下 \(n-1\) 個選手。假如此時把 \(k\) 看作 \(0'\)\(k+1\) 看作 \(1'\) ... 則對應有:

      0  1 2 3 ... k-2  # k  k+1 k+2 ... n-1
    n-k'   ...     n-2'   0'  1'  2' ... n-k-1'
    

    此時在 \(0',1',...,n-2'\) 上再進行一次 \(k\) 報數的選擇。而 \(f[n-1]\) 的值已經求得,因此我們可以直接求得當選者的編號 \(s'\)

    但是,該編號 \(s'\) 是在 \(n-1\) 個候選人報數時的編號,並不等於 \(n\) 個人時的編號,所以我們還需要將\(s'\) 轉換為對應的 \(s\) 。通過觀察,\(s\)\(s'\) 編號相對偏移了 \(k\) ,又因為是在環中,因此得到 \(s = (s'+k) \bmod n\) ,即 \(f[n] = (f[n - 1] + k) \pmod n\)

    然后就證明完畢了。

這個時間復雜度就是 \(O(n)\) 的了,算比較優秀了,但對於這題來說還是不行。

遞推二

因此當 \(n\) 小於 \(k\) 時,就只能采用第一種遞推的算法來計算了。

那么我們就可以用第二種遞推,解決的思路仍然和上面相同,而區別在於我們每次減少的 \(n\) 的規模不再是 \(1\)

同樣用一個例子來說明,初始 \(n=10,k=4\) :

初始序列:

0 1 2 3 4 5 6 7 8 9

\(7\) 號進行過報數之后:

0 1 2 - 4 5 6 - 8 9

而對於任意一個 \(n,k\) 來說,退出的候選人數量為 \(\displaystyle \lfloor \frac{n}{k} \rfloor\)

由於此時起點為 \(8\) ,則等價於:

2 3 4 - 5 6 7 - 0 1

因此我們仍然可以從 \(f[8]\) 的結果來推導出 \(f[10]\) 的結果。

我們需要根據 \(f[8]\) 的值進行分類討論。假設 \(f[8]=s\) ,則根據 \(s\)\(n \bmod k\) 的大小關系有兩種情況:

\[\begin{cases} s' = s - n \bmod k + n & (s< n \bmod k) \\ \displaystyle s' = s - n \bmod k + \lfloor \frac{(s - n \bmod k)}{k-1} \rfloor & (s \ge n \bmod k) \end{cases} \]

上面那種就是最后剩的的幾個數,對於上面例子就是 \(s=\{0, 1\}\)\(s' = \{8, 9\}\)

下面那種就是其余的幾個數,對於上面例子就是 \(s = \{5,6,7\}\)\(s' = \{4,5,6\}\)

這兩種就是先定位好初始位置,然后再加上前面的空隙,得到新的位置。

由於我們不斷的在減小 \(n\) 的規模,最后一定會將 \(n\) 減少到小於 \(k\) ,此時 \(\displaystyle \lfloor \frac{n}{k} \rfloor = 0\)

因此當 \(n\) 小於 \(k\) 時,就只能采用第一種遞推的算法來計算了。

每次我們將 \(\displaystyle n \to n - \lfloor \frac{n}{k} \rfloor\) ,我們可以近似看做把 \(n\) 乘上 \(\displaystyle 1-\frac{1}{k}\)

按這樣分析的話,復雜度就是 \(\displaystyle O(-\log_{\frac{k-1}{k}}n+k)\) 的。這個對於 \(k\) 很小的時候會特別快。

代碼實現

int Josephus(int n, int k) {
	if (n == 1) return 0;
	int res = 0;
	if (n < k) {
		For (i, 2, n)
			res = (res + k) % i;
		return res;
	}
	res = Josephus(n - n / k, k);
	if (res < n % k) 
		res = res - n % k + n;
	else 
		res = res - n % k + (res - n % k) / (k - 1);
	return res;
}

擴展歐幾里得

問題描述

對於三個自然數 \(a,b,c\) ,求解 \(ax+by = c\)\((x, y)\) 的整數解。

算法解決

首先我們要判斷是否存在解,對於這個這個存在整數解的充分條件是 \(\gcd(a, b)~ |~c\)

也就是說 \(c\)\(a,b\) 最大公因數的一個倍數。

朴素歐幾里得

對於求解 \(\gcd(a, b)\) 我們需要用 朴素歐幾里得定理 。

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

這個是比較好證明的:

假設 \(a = k*b+r\) ,有 \(r = a \bmod b\) 。不妨設 \(d\)\(a\)\(b\) 的一個任意一個公約數,則有 \(a \equiv b \equiv 0 \pmod d\)
由於同余的性質 \(a-kb \equiv r \equiv 0 \pmod d\) 因此 \(d\)\(b\)\(a \bmod b\) 的公約數。
然后 \(\forall~ d~|\gcd(a, b)\) 都滿足這個性質,所以這個定理成立啦。

所以我們就可以得到算 \(\gcd\) 的一個簡單函數啦。

int gcd(int a, int b) {
    return !b ? a : gcd(b, a % b);
}

這個復雜度是 \(O(\log)\) 的,十分迅速。

然后判定是否有解后,我們需要在這個基礎上求一組解 \((x, y)\) ,由於 \(a,b,c\) 都是 \(\gcd(a,b)\) 的倍數。

對於 \(a, b\) 有負數的情況,我們需要將他們其中一個負數加上另外一個數直到非負。(由於前面朴素歐幾里得定理是不會影響的)兩個負數,直接將整個式子反號,然后放到 \(c\) 上就行了。

我們將它們都除以 \(\gcd(a, b)\) ,不影響后面的計算。

也就是我們先求對於 \(ax + by = 1\)\(a \bot b\) (互質)求 \((x, y)\) 的解。

接下來我們利用前面的朴素歐幾里得定律推一波式子。

\[\begin{align} ax+by&=\gcd(a,b)\\ &=\gcd(b, a \bmod b) \\ &\Rightarrow bx + (a \bmod b)~ y \\ &= bx + (a - \lfloor \frac{a}{b} \rfloor ~b)~y \\ &= a y + b~(x - \lfloor \frac{a}{b} \rfloor ~y) \end{align} \]

不難發現此時 \(x\) 變成了 \(y\)\(y\) 變成了 \(\displaystyle x - \lfloor \frac{a}{b} \rfloor ~y\) ,利用這個性質,我們可以遞歸的去求解 \((x,y)\)

邊界條件其實和前面朴素歐幾里得是一樣的 \(b=0\) 的時候,我們有 \(a=1,ax+by=1\) 那么此時 \(x = 1, y = 0\)

這樣做完的話我們用 \(O(\log)\) 的時間就會得到一組 \((x, y)\) 的特殊解。

解系擴展

但常常我們要求對於 \(x ~or~ y\) 的最小非負整數解,這個的話我們需要將單個 \((x, y)\) 擴展成一個解系。

如果學過不定方程的話,就可以輕易得到這個解系的,在此不過多贅述了。

\[d\in \mathbb{Z}\\ \begin{cases} x = x_0 + db \\ y = y_0 - da \\ \end{cases} \]

要記住,\((x, y)\) 都需要乘上 \(c\)

然后我們直接令 \(x\)\((x \bmod b + b) \bmod b\) 就行了。

代碼實現

超簡短版本:

遞歸調用的話, \(y=x', x = y'\) ,只需要修改 \(y\) 就行了。(是不是很好背啊)

void Exgcd(ll a, ll b, ll &x, ll &y) {
	if (!b) x = 1, y = 0;
	else Exgcd(b, a % b, y, x), y -= a / b * x;
}

歐拉函數

定義

我們定義 \(\varphi (x)\) 為 小於 \(x\) 的正整數中與 \(x\) 互質的數的個數,稱作歐拉函數。數學方式表達就是

\[\varphi(x) = \sum_{i < x} [i \bot x] \]

但需要注意,我們定義 \(\varphi(1) = 1\)

性質

  1. \(x\) 為質數,\(\varphi(x) = x - 1\)

    證明:這個很顯然了,因為除了質數本身的數都與它互質。

  2. \(x = p^k\)\(p\) 為質數, \(x\) 為單個質數的整數冪),則 \(\varphi(x) = (p - 1) \times p ^ {k - 1}\)

    證明:不難發現所有 \(p\) 的倍數都與 \(x\) 不互質,其他所有數都與它互質。

    \(p\) 的倍數剛好有 \(p^{k-1}\) 個(包括了 \(x\) 本身)。

    那么就有 \(\varphi(x) = p^k - p^{k-1} = (p - 1) \times p^{k - 1}\)

  3. \(p, q\) 互質,則有 \(\varphi(p \times q) = \varphi(p) \times \varphi(q)\) ,也就是歐拉函數是個積性函數。

    證明 :

    如果 \(a\)\(p\) 互質 \((a<p)\)\(b\)\(q\) 互質 \((b<q)\)\(c\)\(pq\) 互質 \((c<pq)\) ,則 \(c\) 與數對 \((a,b)\) 是一一對應關系。由於 \(a\) 的值有 \(\varphi (p)\) 種可能, \(b\) 的值有 \(\varphi(q)\) 種可能,則數對 \((a,b)\)\(φ(p)φ(q)\) 種可能,而 \(c\) 的值有 \(φ(pq)\) 種可能,所以 \(φ(pq)\) 就等於 \(φ(p)φ(q)\)

    具體來說這一條需要 中國剩余定理 以及 完全剩余系 才能證明,感性理解一下它的思路吧。(后面再填)

  4. 對於一個正整數 \(x\) 的質數冪分解 \(\displaystyle x = {p_1}^{k_1} \times {p_2}^{k_2} \times \cdots \times {p_{n} }^{k_n} = \prod _{i=1}^{n} {p_i}^{k_i}\)

    \[\displaystyle \varphi(x) = x \times (1 - \frac{1}{p_1}) \times (1 - \frac{1}{p_2}) \times \cdots \times (1 - \frac{1}{p_n}) = x~\prod_{i=1}^{n} (1-\frac{1}{p_i}) \]

    證明:

    我們考慮用前幾條定理一起來證明。

    \[\begin{align} \varphi(x) &= \prod_{i=1}^{n} \varphi(p_i^{k_i}) \\ &= \prod_{i=1}^{n} (p_i-1)\times {p_i}^{k_i-1}\\ &=\prod_{i=1}^{n} {p_i}^{k_i} \times(1 - \frac{1}{p_i})\\ &=x~ \prod_{i=1}^{n} (1- \frac{1}{p_i}) \end{align} \]

  5. \(p\)\(x\) 的約數( \(p\) 為質數, \(x\) 為任意正整數),我們有 $\varphi(x \times p) = \varphi(x) \times p $ 。

    證明:

    我們利用之前的第 \(4\) 個性質來證明就行了。

    不難發現 \(\displaystyle \prod _{i=1}^{n} (1 - \frac{1}{p_i})\) 是不會變的,前面的那個 \(x\) 會變成 \(x \times p\)

    所以乘 \(p\) 就行了。

  6. \(p\) 不是 \(x\) 的約數( \(p\) 為質數, \(x\) 為任意正整數),我們有 \(\varphi(x \times p) = \varphi(x) \times (p - 1)\)

    證明 :\(p, x\) 互質,由於 \(\varphi\) 積性直接得到。

求歐拉函數

求解單個歐拉函數

我們考慮質因數分解,然后直接利用之前的性質 \(4\) 來求解。

快速分解的話可以用前面講的 \(Pollard~Rho\) 算法。

求解一系列歐拉函數

首先需要學習 線性篩 ,我們將其改一些地方。

質數 \(p\)\(\varphi(p) = p-1\)

對於枚舉一個數 \(i\) 和另外一個質數 \(p\) 的積 \(x\)

我們在線性篩,把合數篩掉會分兩種情況。

  1. \(p\) 不是 \(i\) 的一個約數,由於性質 \(5\) 就有 \(\varphi(x) = \varphi(i) \times (p - 1)\)
  2. \(p\)\(i\) 的一個約數,此時我們會跳出循環,由於性質 \(6\)\(\varphi(x) = \varphi(i) \times p\)

代碼實現

bitset<N> is_prime; int prime[N], phi[N], cnt = 0;
void Init(int maxn) {
	is_prime.set(); is_prime[0] = is_prime[1] = false; phi[1] = 1;
	For (i, 2, maxn) {
		if (is_prime[i]) phi[i] = i - 1, prime[++ cnt] = i;
		For (j, 1, cnt) {
			int res = i * prime[j];
			if (res > maxn) break;
			is_prime[res] = false;
			if (i % prime[j]) phi[res] = phi[i] * (prime[j] - 1);
			else { phi[res] = phi[i] * prime[j]; break; }
		}
	}
}

擴展歐拉定理

\[a^b\equiv \begin{cases} a^{b \bmod \varphi(p)} & a \bot p\\ a^b & a \not \bot p,b<\varphi(p)\\ a^{b \bmod \varphi(p) + \varphi(p)} & a \not \bot p,b\geq\varphi(p) \end{cases} \pmod p \]

證明看 這里 ,有點難證,記結論算啦qwq

這個常常可以用於降冪這種操作。它的一個擴展就是最前面的那個 費馬小定理

求解模線性方程組

問題描述

給定了 \(n\) 組除數 \(m_i\) 和余數 \(r_i\) ,通過這 \(n\)\((m_i,r_i)\) 求解一個 \(x\) ,使得 \(x \bmod m_i = r_i\)  這就是 模線性方程組

數學形式表達就是 :

\[\begin{cases} x \equiv r_1 \pmod {m_1}\\ x \equiv r_2 \pmod {m_2}\\ ~~~~\vdots \\ x \equiv r_n \pmod {m_n} \end{cases} \]

求解一個 \(x\) 滿足上列所有方程。

算法解決

中國剩余定理

中國剩余定理提供了一個較為通用的解決方法。(似乎是唯一一個以中國來命名的定理)

如果 \(m_1, m_2, \dots , m_n\) 兩兩互質,則對於任意的整數 \(r_1, r_2 , \dots , r_n\) 方程組 \((S)\) 有解,並且可以用如下方法來構造:

\(\displaystyle M = \prod_{i=1} ^ {n} m_i\) ,並設 \(\displaystyle M_i = \frac{M}{m_i}\) ,令 \(t_i\)\(M_i\) 在模 \(m_i\) 意義下的逆元(也就是 \(t_i \times M_i \equiv 1 \pmod {m_i}\) )。

不難發現這個 \(t_i\) 是一定存在的,因為由於前面要滿足兩兩互質,那么 \(M_i \bot m_i\) ,所以必有逆元。

那么在模 \(M\) 意義下,方程 \((S)\) 有且僅有一個解 : \(\displaystyle x = (\sum_{i=1}^{n} r_i t_i M_i) \bmod M\)

不難發現這個特解是一定滿足每一個方程的,只有一個解的證明可以考慮特解 \(x\) 對於第 \(i\) 個方程,下個繼續滿足條件的 \(x\)\(x + m_i\) 。那么對於整體來說,下個滿足條件的數就是 \(x + \mathrm{lcm} (m_1, m_2, \dots, m_n) = x + M\)

嚴謹數學證明請見 百度百科

利用擴歐合並方程組

我們考慮合並方程組,比如從 \(n=2\) 開始遞推。

\[\begin{cases} x \equiv r_1 \pmod {m_1}\\ x \equiv r_2 \pmod {m_2} \end{cases} \]

也就等價於

\[\begin{cases} x = m_1 \times k_1 + r_1 \\ x = m_2 \times k_2 + r_2 \\ \end{cases} \]

此處 \(k_1, k_2 \in \mathbb{N}\) 。聯立后就得到了如下一個方程:

\[\begin{align} m_1 \times k_1 + r_1 &= m_2 \times k_2 + r_2 \\ m_1 \times k_1 - m_2 \times k_2 &= r_2 - r_1 \end{align} \]

我們令 \(a = m_1, b = m_2, c = r_2 - r_1\) 就變成了 \(ax+by=c\) 的形式,用之前講過的 擴展歐幾里得 ,可以求解。

首先先判斷有無解。假設存在解,我們先解出一組 \((k_1,k_2)\) ,然后帶入解出 \(x=x_0\) 的一個特解。

我們將這個可以擴展成一個解系:

\[x = x_0 + k \times \mathrm{lcm} (m_1,m_2) ~,~ k \in \mathbb{N} \]

由於前面不定方程的結論, \(k_1\) 與其相鄰解的間距為 \(\displaystyle \frac{m_2}{\gcd(m_1,m_2)}\) ,又有 \(x=m_1 \times k_1 + r_1\) 。所以 \(x\) 與其相鄰解的距離為 \(\displaystyle \frac{m_1 m_2}{\gcd(m_1,m_2)} = \mathrm{lcm}(m_1,m_2)\)

所以我們令 \(M=\mathrm{lcm}(m_1, m_2), R = x_0\) 則又有新的模方程 \(x \equiv R \pmod M\)

然后我們就將兩個方程合並成一個了,只要不斷重復這個過程就能做完了。

這個比 中國剩余定理 優秀的地方就在於它這個不需要要求 \(m\) 兩兩互質,並且可以較容易地判斷無解的情況。

代碼實現

注意有些細節,比如求兩個數的 \(\mathrm{gcd}\) 的時候,一定要先除再乘,防止溢出!!

ll mod[N], rest[N];
ll Solve() {
	For (i, 1, n - 1) {
		ll a = mod[i], b = mod[i + 1], c = rest[i + 1] - rest[i], gcd = __gcd(a, b), k1, k2;
		if (c % gcd) return - 1;
		a /= gcd; b /= gcd; c /= gcd;
		Exgcd(a, b, k1, k2);

		k1 = ((k1 * c) % b + b) % b;
		mod[i + 1] = mod[i] / __gcd(mod[i], mod[i + 1]) * mod[i + 1] ;
		rest[i + 1] = (mod[i] * k1 % mod[i + 1] + rest[i]) % mod[i + 1];
	}
	return rest[n];
}

擴展盧卡斯

問題描述

\[{n \choose m} \bmod p \]

\(1 \le m \le n \le 10^{18}, 2 \le p \le 10^6\) 不保證 \(p\) 為質數。

問題求解

雖說是擴展盧卡斯,但是和盧卡斯定理沒有半點關系。

用了幾個性質,首先可以考慮 \(p\) 分解質因數。

假設分解后

\[p = \prod_i {p_i}^{k_i} \]

我們可以對於每個 \(p_i\) 單獨考慮,假設我們求出了 \(\displaystyle {n \choose m} \bmod {{p_i}^{k_i}}\) 的值。

然后我們可以考慮用前文提到的 \(CRT\) 來進行合並(需要用擴歐求逆元)。

問題就轉化成如何求 \(\displaystyle {n \choose m} \bmod {{p_i}^{k_i}}\) 了。

首先我們考慮它有多少個關於 \(p_i\) 的因子(也就是多少次方)。

我們令 \(f(n)\)\(n!\) 中包含 \(p_i\) 的因子數量,那么 \(\displaystyle {n \choose m} = \frac{n!}{m!(n - m)!}\) ,所以因子數量就為 \(f(n) - f(m) - f(n - m)\)

那如何求 \(f(n)\) 呢。

我們舉出一個很簡單的例子來討論。

\(n=19,p_i=3,k_i=2\) 時:

\[\begin{align} n!&=1*2*3*\cdots*19\\ &=(1∗2∗4∗5∗7∗8∗10∗11∗13∗14∗16∗17∗19)∗3^6∗6!\\ &\equiv (1*2*4*5*7*8)^2*19*3^6*6!\\ &\equiv (1∗2∗4∗5∗7∗8)^2∗19∗{3^6}∗6! \pmod {3^2} \\ \end{align} \]

不難發現每次把 \(p_i\) 倍數提出來的東西,就是 \(\displaystyle \lfloor \frac{n}{p_i} \rfloor !\) ,那么很容易得到如下遞推式:

\[f(n) = f(\displaystyle \lfloor \frac{n}{p_i} \rfloor) + \displaystyle \lfloor \frac{n}{p_i} \rfloor \]

不難發現這個求單個的復雜度是 \(O(\log n)\) 的,十分優秀。

然后考慮剩下與 \(p_i\) 互不相關的如何求,不難發現在 \({p_i}^{k_i}\) 意義下會存在循環節(比如前面的 \((1∗2∗4∗5∗7∗8)^2\) ,可能最后多出來一個或者多個不存在循環節中的數。

不難發現循環節長度是 \(< {p_i}^{k_i}\) ,因為同余的在 \(> {p_i}^{k_i}\) 之后馬上出現,然后把多余的 \(\displaystyle \lfloor \frac{n}{p_i} \rfloor\) 的部分遞歸處理就行了。

然后就可以快速處理出與 \(p_i\) 無關的了,最后合並一下就行了。

簡介算法流程:

  • 對於每個 \({p_i}^{k_i}\) 單獨考慮。
    • \(f(n)\) 處理出有關於 \(p_i\) 的因子個數。
    • 然后遞歸處理出無關 \(p_i\) 的組合數的答案。
    • 把這兩個乘起來合並即可。
  • 最后用 \(CRT\) 合並所有解即可。

瞎分析的復雜度( Great_Influence 博客上抄的)。(不可靠)

\[O(\sum {p_i}^{k_i} \log n(\log_{p_i} n - k)+p_i \log p) \sim O(p \log p) \]

代碼解決

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << (x) << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

typedef long long ll;

template<typename T> inline bool chkmin(T &a, T b) { return b < a ? a = b, 1 : 0; }
template<typename T> inline bool chkmax(T &a, T b) { return b > a ? a = b, 1 : 0; }

void File() {
#ifdef zjp_shadow
	freopen ("P4720.in", "r", stdin);
	freopen ("P4720.out", "w", stdout);
#endif
}

ll n, m, p;

void Exgcd(ll a, ll b, ll &x, ll &y) {
	if (!b) x = 1, y = 0;
	else Exgcd(b, a % b, y, x), y -= a / b * x;
}

inline ll fpm(ll x, ll power, ll Mod) {
	ll res = 1;
	for (; power; power >>= 1, (x *= x) %= Mod)
		if (power & 1) (res *= x) %= Mod;
	return res;
}

inline ll fac(ll n, ll pi, ll pk) {
	if (!n) return 1;
	ll res = 1;
	For (i, 2, pk) if (i % pi) (res *= i) %= pk;
	res = fpm(res, n / pk, pk);
	For (i, 2, n % pk) if (i % pi) (res *= i) %= pk;
	return res * fac(n / pi, pi, pk) % pk;
}

inline ll Inv(ll n, ll Mod) {
	ll x, y; Exgcd(n, Mod, x, y);
	return (x % Mod + Mod) % Mod;
}

inline ll CRT(ll b, ll Mod) {
	return b * Inv(p / Mod, Mod) % p * (p / Mod) % p;
}

inline ll factor(ll x, ll Mod) {
	return x ? factor(x / Mod, Mod) + (x / Mod) : 0;
}

inline ll Comb(ll n, ll m, ll pi, ll pk) {
	ll k = factor(n, pi) - factor(m, pi) - factor(n - m, pi);
	if (!fpm(pi, k, pk)) return 0;
	return fac(n, pi, pk) * Inv(fac(m, pi, pk), pk) % pk * Inv(fac(n - m, pi, pk), pk) % pk * fpm(pi, k, pk) % pk;
}

inline ll ExLucas(ll n, ll m) {
	ll res = 0, tmp = p;
	For (i, 2, sqrt(p + .5)) if (!(tmp % i)) {
		ll pk = 1; while (!(tmp % i)) pk *= i, tmp /= i;
		(res += CRT(Comb(n, m, i, pk), pk)) %= p;
	}
	if (tmp > 1) (res += CRT(Comb(n, m, tmp, tmp), tmp)) %= p;
	return res;
}

int main () {

	File();

	cin >> n >> m >> p;

	printf ("%lld\n", ExLucas(n, m));

	return 0;

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM