Lecturer : Lemon
好久沒講課ㄌw
這個學期我只有上演算法ㄉ課ouo
所以比較輕鬆一點ㄅ
希望講課的品質能夠++
稍微講一下考幹好ㄌ
演算法應該會是我們決定學術們最主要的途徑
所以 加油 (?
學術力 UP UP !!!
大家應該都知道質數是什麼ㄅ
不是植樹也不是直樹ㄉ那個
只有 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(QN)\)
延續剛剛的想法
我們發現只要窮舉\(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)\)ㄉ
可以直接背結論ㄛ
證明我會用講ㄉ(? 雖然我不會數學Q
\(\sum^{n}_{i = 1} \frac{1}{i} \in O(log n) \)
質數的個數 \(\in O(\frac{n}{logn})\)
至於為甚麼剛剛的複雜度那麼毒
可以看這ㄍ
仔細觀察迴圈
你會發現對每個質數\(p\)都會需要跑將近\(\frac{n}{p}\)次
複雜度 \(= \sum_{p \leq n} \frac{n}{p}\)
所以我們要算數學Q
幾乎在所有時候埃氏篩的\(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掛ㄌQ LIOJ 1020
簡單來說就是除以\(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
啊我就不證明ㄌ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
沒關係
你們還是要會ouo
有整數解
大概就這樣(?
當\(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);
}
一般的輾轉相除法
我們知道在程式運行時當\(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\)
故
就求出模反元素ㄌ
複雜度同歐幾里得算法:\(O(log n)\)
要證明很難Q
有物不知其數,三三數之剩二,五五數之剩三,七七數之剩二。問物幾何?
數學表示:
三人同行七十希,五樹梅花廿一支,七子團圓正半月,除百零五便得知
即\(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}\)
沒有飯粒程式碼
因為都學過了(?
可以嘗試自己拼湊看看
早上好中國剩餘定理
我最喜歡模反元素