數論+

Lecturer : Lemon

今天會教的咚咚

模運算

組合計數

中國剩餘定理

質數篩法

積性函數

模運算

簡單來說就是除以\(N\)的餘數 (?

我懶得打

模運算

要注意的是

C++模數的結果有可能是負的

e.g.

(-10) % 3 = -1

反正牽涉模運算時處理都要小心一點

模運算

自從去過校培後

我自己會把模運算寫成函式

避免出錯啦

int mabs(long long a, int mod) { //轉成 0 <= a < mod的形式
    return (a % mod + mod) % mod;
}
int madd(long long a, long long b, int mod) { // a + b
    return mabs(a % mod + b % mod, mod);
}
int mmin(long long a, long long b, int mod) { // a - b
    return mabs(a % mod - b % mod, mod);
}
int mmul(long long a, long long b, int mod) {
    return mabs((a % mod)*(b % mod), mod);
}

模除(?

你會發現剛剛沒有除法Q

因為在模運算中我們不能直接使用除法

e.g.

\(2 \equiv 5\ (mod\ 3)\)

如果用除法的話

\(0.4 \equiv 1\ (mod\ 3)\)

顯然是爛ㄉ

模反元素

就是倒數、反矩陣的概念

假設 \(a \equiv k\ (mod\ p)\)

它的模反元素\(a^{-1}\)使得

\(a \times a^{-1} \equiv k \times a^{-1} \equiv 1\ (mod\ p)\)

啊要怎麼算出來ㄋ(?

我也不知道Q

費馬小定理

\textit{Let}\ a \in \mathbb{Z},\ p\ \textit{is a prime}, \\ a ^ {p} \equiv a\ (mod\ p)

啊我就不證明ㄌw

我們可以從此推得

\(a \times a^{p-2} \equiv 1\ (mod\ p)\)

對照上一頁你便會發現

\(a^{p-2} \equiv a^{-1}\ (mod\ p)\)

費馬小定理

於是

我們套用上上次學的快速冪

便能在\(O(logn)\)下求出模反元素

#include <iostream>
using namespace std;
const int MOD = 1e9 + 7; //常見的 10^9 + 7 就是個質數
int mabs(long long a, int mod = MOD) {
    return (a % mod + mod) % mod;
}
int madd(long long a, long long b, int mod = MOD) { // a + b
    return mabs(a % mod + b % mod, mod);
}
int mmin(long long a, long long b, int mod = MOD) { // a - b
    return mabs(a % mod - b % mod, mod);
}
int mmul(long long a, long long b, int mod = MOD) {
    return mabs((a % mod)*(b % mod), mod);
}
int power(int a, int b) { //快速冪
    int ret = 1;
    while(b) {
        if(b & 1) ret = mmul(ret, a);
        b >>= 1;
        a = mmul(a, a);
    }
    return ret;
}
int main() {
    int a;
    cin >> a;
    cout << mmul(a, power(a, MOD-2)) << '\n'; //這項恆為1
    cout << "INV:" << power(a, MOD-2) << '\n';
    return 0; 
}

歐拉定理

若\(a, n \in \mathbb{Z},\ gcd(a, n) = 1\)

則\(a^{\phi(n)} \equiv 1\ (mod\ n)\)

其中\(\phi(n)\)稱為歐拉函數(後面會再提到)

其值為小於\(n\)且與\(n\)互質的正整數個數

顯而易見的

若p是質數,則\(\phi(p) = p - 1\)

等同於費馬小定理

這幾乎不會用到 但很潮(?

如果模的數不是質數怎麼辦Q

那就沒辦法ㄌQ

我們當然有更加通用的方法

但是比較難寫Q

沒關係

你們還是要會ouo

貝祖定理

ax + by = gcd(a, b)

有整數解

大概就這樣(?

當\(a, b \in \mathbb{Z}\)

擴展歐幾里得算法

這才是重點w

我們可以透過擴展歐幾里得算法計算貝祖定理的解

ㄛ如果你不知道的話 歐幾里得算法就是輾轉相除法

int gcd(int a, int b) {
    if(a > b) return gcd(b, a);
    if(!b) return a;
    return gcd(b, a%b);
}

一般的輾轉相除法

擴展歐幾里得算法

\begin{cases} ax_1 + by_1 = gcd(a, b) \\ bx_0 + (a\%b)y_0 = gcd(a, b) \end{cases} \\ \text{又}a\%b = a - b \times \lfloor \frac{a}{b} \rfloor \\ \text{解聯立得} \begin{cases} x_1 = y_0 \\ y_1 = x_0 - \lfloor \frac{a}{b} \rfloor y_0 \end{cases}

我們知道在程式運行時當\(b = 0\)時\(gcd(a, b) = a\)

故遞迴結束時 我們會知道\(x = 1, y = 0\)

反覆往上推就能找到答案ㄌ

擴展歐幾里得算法

#include <iostream>
using namespace std;
int exgcd(int a, int b, int& x, int& y) { //加參考才能由下而上改變x, y
    if(a < b) return exgcd(b, a, y, x);
    if(!b) {
        x = 1;
        y = 0;
        return a;
    }
    int ret = exgcd(b, a%b, x, y);
    int temp = x; //暫存x0
    x = y;
    y = temp - a / b * y;
    return ret;
}
int main() {
    int a, b, x, y;
    cin >> a >> b;
    cout << exgcd(a, b, x, y) << '\n';
    cout << a * x + b * y << '\n'; //貝祖定理
}

我們便透過擴展歐幾里得算法求得貝祖定理ㄉ一個整數解

貝祖定理

有什麼用(?

若\(gcd(a, b) = 1\)

ax + by = gcd(a, b) = 1
ax \equiv 1\ (mod\ b)

x \equiv a^{-1}\ (mod\ b)

就求出模反元素ㄌ

複雜度同歐幾里得算法:\(O(log n)\)

例題

組合計數

P^n_m = (n - m)!\\
C^n_m = \binom{n}{m} =\frac{n!}{(n-m)!\ \times\ m!}\\
H^n_m = \left( \binom{n}{m} \right) = \binom{n + m - 1}{n}

階乘

const int MAXN = 1e6 + 5, MOD = 1e9 + 7;
using ll = long long int;
int mabs(ll a) {
	return (a % MOD + MOD) % MOD;
}
int mmul(int a, int b) {
	return mabs(1ll * a * b);
}
int fac[MAXN];
void getfac() {
	fac[0] = 1;	
	for(int i = 1; i < MAXN; ++i) {
		fac[i] = mmul(fac[i - 1], i);
	}
}

題目常常會要求我們模一個數字

模運算會在後面教到ㄛ

複雜度:\(O(n)\)

組合數

巴斯卡定理

\binom{n}{m} = \binom{n - 1}{m - 1} + \binom{n - 1}{m}

組合數

using ll = long long int;
const int MAXN = 2005, MOD = 1e9 + 7;
int mabs(ll a) {
	return (a % MOD + MOD) % MOD;
}
int madd(int a, int b) {
	return mabs(a + b);
}
int comb[MAXN][MAXN];
void getcomb() {
	for(int i = 0; i < MAXN; ++i) {
		comb[i][0] = 1;
	}
	for(int i = 1; i < MAXN; ++i) {
		for(int j = 1; j <= i; ++j) {
			comb[i][j] = madd(comb[i - 1][j - 1], comb[i - 1][j]);
		}
	}
}

複雜度:\(O(n^2)\)

考慮利用模逆元(?

重複組合

考慮一個問題:

有\(n\)個球,要分給\(m\)個人,有幾種可能性?

設第\(i\)人分得\(x_i\)個球,則列成數學式如下:

\sum^m_{i = 1}{x_i} = x_1 + x_2 + \dots + x_m = n, x_i \in \mathbb{N^0}

問題變為求\(x\)的非負整數解個數!

重複組合

對\(n\)個球設置\(m - 1\)個隔板

第\(i\)和\(i - 1\)個隔板之間的球數即為\(x_i\)

e.g.

有6個球、3個人

OOOOOO||

的不盡相異物排列數 = \( \left( \binom{6}{3} \right) = \binom{8}{2} \)

對球和隔板做不盡相異物排列!

可能性數:\(\left( \binom{n}{m} \right) = \binom{n + m - 1}{n}\)

重複組合

但只能求非負整數解有什麼用(?

考慮加上限制條件的情況:

x + y + z = n,\ x \geq k

我們使\(x' = x - k\),則\(x' \geq 0\)

x' + y + z = n - k

就可以套用原本的公式了ouo

例題

OJDL 7158 (噁)

中國剩餘定理

有物不知其數,三三數之剩二,五五數之剩三,七七數之剩二。問物幾何?

\begin{cases} x \equiv 2\ (mod\ 3)\\ x \equiv 3\ (mod\ 5)\\ x \equiv 2\ (mod\ 7) \end{cases}

數學表示:

中國剩餘定理

三人同行七十希,五樹梅花廿一支,七子團圓正半月,除百零五便得知

2 \times 70 + 3 \times 21 + 2 \times 15 = 233 = 105 \times 2 + 23

即\(x = 23 + 105k\), \(k \in \mathbb{N^0}\)

所以要怎麼算(?

其實技巧我們都會ㄌ

中國剩餘定理

分開處理每一行式子

假設該行式子長這樣:

\(x \equiv b\ (mod\ p)\)

找到除了\(p\)其他模數的最小公倍數a.k.a \(lcm\)

求模\(p\)下的\(lcm^{-1}\)

\(a_i = lcm \times lcm^{-1}\)在該行的\(b_i = 1\)

其他行的\(b_j = 0\), \(i \neq j\)

\(x =  \sum_{i = 1}^{n} a_i + 全部的lcm \times k\), \(k \in \mathbb{Q}\)

中國剩餘定理

e.g. 原題

\begin{cases} x \equiv 2\ (mod\ 3)\\ x \equiv 3\ (mod\ 5)\\ x \equiv 2\ (mod\ 7) \end{cases}

\(lcm(5, 7) = 35\),\(35^{-1} \equiv 2 \pmod{3}\),\(70 = 35 \times 2 \equiv 1 \pmod{3}\)

\(lcm(3, 7) = 21\),\(21^{-1} \equiv 1 \pmod{5}\),\(21 = 21 \times 1 \equiv 1 \pmod{5}\)

\(lcm(3, 5) = 15\),\(15^{-1} \equiv 1 \pmod{7}\),\(15 = 15 \times 1 \equiv 1 \pmod{7}\)

2 \times 70 + 3 \times 21 + 2 \times 15 = 233 = 105 \times 2 + 23

中國剩餘定理

沒有飯粒程式碼

因為都學過了(?

可以嘗試自己拼湊看看

早上好中國剩餘定理

我最喜歡模反元素

例題

ZJ c641

ZJ c637

基本上根本是同一題(?

質數

大家應該都知道質數是什麼ㄅ

不是植樹也不是直樹ㄉ那個

只有 1 和該數本身兩個因數的自然數

歐幾里得定理

證明了這樣的數字有無限多個

ㄛ這個不用知道也沒關係其實

篩法

為了有效率的判斷一個數字是否是質數

人們致力於發明不同的算法來解決此問題

我們接下來會一步一步ㄉ改進複雜度ㄛㄛㄛ

我們假設一個共同的問題:

有\(Q\)筆詢問,每次詢問一個正整數\(N\)是否是質數

暴力

如果要檢查某個數字\(N\)是否是質數

如果\(N = 1\)那輸出No

否則我們只要檢查\(2 \leq i < N\)的\(i\)是否整除\(N\)即可

#include <iostream>
using namespace std;
int main() {
    int Q;
    cin >> Q;
    for(int q = 0; q < Q; ++q) {
        int n;
        cin >> n;
        if(n == 1) cout << "No\n";
        else {
            bool flag = 1;
            for(int i = 2; i < n; ++i) {
                if(!(n % i)) {
                    flag = 0;
                    break;
                }
            }
            if(flag) cout << "Yes\n";
            else cout << "No\n";
        }
    }
}

複雜度:\(O(NQ)\)

沒那麼暴力ㄉ暴力

延續剛剛的想法

我們發現只要窮舉\(2 \leq i \leq \sqrt{N}\)

因為他若不是質數,一定會被小的數先整除掉

#include <iostream>
#include <cmath>
using namespace std;
int main() {
    int Q;
    cin >> Q;
    for(int q = 0; q < Q; ++q) {
        int n;
        cin >> n;
        if(n == 1) cout << "No\n";
        else {
            bool flag = 1;
            int mx = sqrt(n);
            for(int i = 2; i <= mx; ++i) {
                if(!(n % i)) {
                    flag = 0;
                    break;
                }
            }
            if(flag) cout << "Yes\n";
            else cout << "No\n";
        }
    }
}

複雜度:\(O(Q\sqrt{N})\)

埃氏篩法

我們並不一定要一個一個詢問慢慢算

可以考慮先把所有答案算出來再回答

對於每個質數

排除掉範圍內ㄉ該質數ㄉ倍數

並且根據剛剛的結論

每個數字\(N\)都只需要被\(\sqrt{N}\)以下的數排除

換句話說

對於質數\(p\),它也只需要排除

\(p^{2} \leq i \leq 值域\) 的\(p\)的倍數

埃氏篩法

#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e6+5; //值域
bool isprime[MAXN];//是否有可能是質數
vector<int> prime;//記錄現在的質數(非必要)
int main() {
    for(int i = 2; i < MAXN; ++i) {
        isprime[i] = 1; //預設為true
    }
    for(int i = 2; i < MAXN; ++i) {
        if(isprime[i]) { //如果i尚未被排除就是質數
            prime.push_back(i);
            for(long long int j = 1ll * i * i; j < MAXN; j += i) { //注意溢位
                isprime[j] = 0;
            }
        }
    }
    int Q;
    cin >> Q;
    for(int q = 0; q < Q; ++q) {
        int n;
        cin >> n;
        if(isprime[n]) cout << "Yes\n";
        else cout << "No\n";
    }
}

複雜度:\(O(NloglogN)\)

反正它幾乎是\(O(N)\)ㄉ

複雜度分析

根據酷酷的數學

\(\sum^{n}_{i = 1} \frac{1}{i} \in O(log n) \)

質數\(p\)的個數 \(\in O(\frac{n}{logn})\)

至於為甚麼剛剛的複雜度那麼毒

可以看這ㄍ

仔細觀察迴圈

你會發現對每個質數\(p\)都會需要跑將近\(\frac{n}{p}\)次

複雜度 \(= \sum_{p \leq n} \frac{n}{p}\)

線性篩法

幾乎在所有時候埃氏篩的\(O(nloglogn)\)都是相當足夠的

在實作上我們也通常使用埃氏篩比較多

但我們很好奇 是否有一種算法是線性的 也就是\(O(n)\)

其實是有ㄉ

嘗試一邊製作質數表,一邊刪掉每個數的質數倍

這樣每個合數只會遇到最小質因數

並且每個合數只會被刪掉一次

線性篩法

#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e6+5; //值域
bool isprime[MAXN];//是否有可能是質數
vector<int> prime;
int main() {
    for(int i = 2; i < MAXN; ++i) {
        isprime[i] = 1;
    }
    for(int i = 2; i < MAXN; ++i) {
        if(isprime[i]) prime.push_back(i);
        for(auto p : prime) {
            if(1ll * i * p > MAXN) break;
            isprime[i * p] = 0;
            if(!(i % p)) break;
        }
    }
    int Q;
    cin >> Q;
    for(int q = 0; q < Q; ++q) {
        int n;
        cin >> n;
        if(isprime[n]) cout << "Yes\n";
        else cout << "No\n";
    }
}

複雜度:\(O(N)\)

例題

質數常常只是題目的一小部分

記得要熟練ouo

理論上應該愈下面愈難(?

ZJ b513

ZJ a010 

ZJ a007

TIOJ 1036

積性函數

若\(a, b\)互質時,恆滿足\(f(ab) = f(a)f(b)\)

則\(f\)稱為積性函數

它會滿足以下性質:

\(f(1) = 1\)

若我們能透過定義得知\(f(p^k)\)

其中\(p\)是質數,\(k \in \mathbb{N^0}\)

那我們就能以\(O(n)\)求出所有的\(f(i)\)

積性函數

該要怎麼求(?

考慮在線性篩的過程維護資訊!

注意到每個數字都會被它最小的質因數篩掉!

積性函數

介紹兩個函數(?

歐拉函數\(\phi\)

\phi(p^k) = (p - 1) \times p^{k - 1}

莫比烏斯函數\(\mu\)

\mu(p^k) = \left\{ \begin{aligned} -1,\ k = 1 \\ 0,\ k > 1 \end{aligned} \right.

積性函數

using ll = long long int;
const int MAXN = 1e6 + 5; 
bool isprime[MAXN];
int mu[MAXN], phi[MAXN];
vector<int> primes;
void sieve() {
	for(int i = 2; i < MAXN; ++i) {
		isprime[i] = 1;
	}
	isprime[1] = 0;
	mu[1] = phi[1] = 1;
	for(int i = 2; i < MAXN; ++i) {
		if(isprime[i]) {
			phi[i] = i - 1;
			mu[i] = -1;
			primes.emplace_back(i);
		}
		for(auto p : primes) {
			if(1ll * i * p >= MAXN) break;
			isprime[i * p] = 0;
			phi[i * p] = phi[i] * phi[p];
			mu[i * p] = mu[i] * mu[p];
			if(!(i % p)) {
				phi[i * p] = phi[i] * p;
				mu[i * p] = 0;
				break;
			}
		}
	}
}

例題

ZJ f568 (墨筆鎢絲函數)

註:

窩本來想講墨筆鎢絲反演

但那似乎有點太難了

所以先不講w