On Github betaveros / 2014-algo
2014/10/19 陳伯恩
http://betaveros.github.io/2014-algo/
(created with reveal.js-2.6.2)
(see: graph)
bool isPrime(int n) { if (n <= 1) return false; for (int p = 2; p * p <= n; p++) { if (n % p == 0) return false; } return true; }
from math import sqrt def isPrime(n): if n <= 1: return False for p in range(2, int(sqrt(n))+1): if n % p == 0: return False return True
function isPrime(n) { if (n <= 1) return false; for (var p = 2; p * p <= n; p++) { if (n % p == 0) return false; } return true; }
isPrime :: (Integral a) => a -> Bool isPrime n | n <= 1 = False | otherwise = all ((/= 0) . (n `mod`)) $ takeWhile ((<= n) . (^2)) [2..]
程式語言超級多,每個人喜好不同
假設你要用Python寫個網站好了
© Joshua Schulter under GPL
當然做其他事情也需要學演算法,但更會遇到很多架構/設計的問題,或是需要學習用各種模組跟API,或是debug。
(Knuth, The Art of Computer Programming)
Source: Wikicommons
這是演算法嗎?
可以解決的問題:
gcd(a, b) = ?
If b = 0 then gcd(a, b) = a If b ≠ 0 then gcd(a, b) = gcd(b, a mod b)「= 0」、「mod」是有限、明確、有效的。
在GCD裡面再計算一次GCD是明確、有效的(做同樣的判斷、計算,直到有答案為止)。
是有限的(為什麼?)
線上賽似乎都很殘忍的只回傳部分訊息
學過的先做一下題目吧。例如挑戰2013 NPSC高中組決賽破台。(先做A, C, G)
int a, b; cin >> a >> b; cout << a + b << endl; cout << a - b << endl; cout << a * b << endl; cout << a / b << endl; // !!! cout << a % b << endl; // mod // 7 / 3 = ? // (-7) / 3 = ? // (-7) % 3 = ?
int x; // -2^31 ~ 2^31 - 1(通常) double y; // 浮點,可表示非整數。不完全精確,所以比較時請小心 // 例:abs(y - yy) < 1e-8 long long z; // -2^63 ~ 2^63 - 1(通常) // 常需要打"long long"都會感到困擾。 typedef long long ll; ll y; // 請不要在競賽外這樣寫
#include <iostream> using namespace std; int cube(int x) { return x * x * x; } int main() { cout << cube(7) << endl; }
順帶一提,競賽爭手速跟思考速度,在程式競賽之外寫using namespace std;其實不是好習慣
int a, b; cin >> a >> b; if (a == b) cout << "相等" << endl; if (a != b) cout << "不相等" << endl; if (a < b) cout << "小於" << endl; if (a > b) cout << "大於" << endl; if (a <= b) cout << "小於等於" << endl; if (a >= b) cout << "大於等於" << endl;
單獨!就是「非」的意思,跟=結合為「不等於」很好記。(晚一點會看到)
#include <iostream> void printTwoFactors(int x) { int c = 0; while (x % 2 == 0) { x /= 2; c++; } // 被執行零至多次,直到x是奇數為止 if (c > 0) { // 執行一次,或不執行 cout << "2^" << c << " * " << x << endl; } else { // if後的else可加可不加 cout << x << endl; } } int main() { for (int i = 0; i < 100; i++) { // 最常用法 printTwoFactors(i); // i會跑遍1到100 } }
int a0, a1, a2, a3, ..., a99;
⇒ int a[100];
a[0], a[1], a[2], ..., a[99];
#include <iostream> int div[100]; int main() { for (int i = 1; i < 100; i++) { for (int j = i; j < 100; j += i) { // j = i, 2i, 3i... div[j]++; } cout << i << ": " << div[i] << endl; } }
初階可以先跳過;大約照重要性跟實用性遞減排序。
我寫的有點過火,不用看完
可以當成一個值或算式的if/else
if (a == 1) { cout << b << endl; } else { cout << (-b) << endl; }
↕
cout << (a == 1 ? b : (-b)) << endl;
另外,==跟其他比較式的結果都也是數字:0(false、假)或1(true、真)
if、while、for、?:都把0當成false,≠0當成true。不過你不知道這件事寫出來的程式會比較可讀。
&&, ||, !
and, or, not
「且」、「或」、「非」
1 && 1 = 1
1 && 0 = 0 && 1 = 0 && 0 = 0
1 || 1 = 1 || 0 = 0 || 1 = 1
0 || 0 = 0
if (a == 0 || b == 0) { cout << "至少有一個0"; }
#include <algorithm> #include <vector> using namespace std; int a[100]; vector<int> v; int main() { for (int i = 0; i < 100; i++) { cin >> a[i]; } for (int i = 0; i < 100; i++) { int x; scanf("%d", &x); v.push_back(x); // ! } sort(a, a + 100); // !! sort(v.begin(), v.end()); // !!! }
雖然cin, cout用起來很順手,不過比賽用它遲早會被雷:
速度慢!
#include <cstdio> using namespace std; int main() { int a, b; scanf("%d%d", &a, &b); printf("%d%d", a, b); }
C留下來的函數,所以傳入scanf的變數需要用&(包成指標)。
不過如果要輸出浮點數的時候⋯⋯
#include <cstdio> #include <iostream> #include <iomanip> using namespace std; int main() { double x; printf("%.15lf\n", x); cout << fixed << setprecision(15) << x << endl; }
對了,還有字串輸入輸出
字串結尾有一個\0 (ascii 0)
#include <cstdio> #include <iostream> using namespace std; char buf[108]; // 小心\r\n\0 int main() { // 機車的事情是這四個東西處理「讀到什麼時候都不一樣 scanf("%100s", buf); // 讀到空白(包括\n),不讀空白本身 // buf已經是指標(陣列),所以不用包 fgets(buf, 100, stdin); // 讀到\n,會讀\n本身,還會放進buf cin.get(buf, 100); // 讀到\n,不讀\n本身 cin.getline(buf, 100); // 讀到\n,會讀\n,但不會寫進buf }
#include <cmath> struct Point { int x; int y; }; Point pts[100]; vector<Point> ptv; double dist(Point a, Point b) { double xd = a.x - b.x; double yd = a.y - b.y; return std::sqrt(xd * xd + yd * yd); }
#include <bits/stdc++.h> struct Point { int x; int y; // ↓↓↓ 這一行基本上要背起來 ↓↓↓ bool operator<(const Point & o) const { if (x != o.x) return x < o.x; return y < o.y; } }; vector<Point> ptv; int main() { // ... 輸入到ptv之類的 ... sort(ptv.begin(), ptv.end()); }
(中文似乎真的翻做「參考」,不過我覺得「別名」比較貼切)
函數要修改外面傳進來的變數的時候(常常用來回傳多個值)
int egcd(int a, int b, int & ac, int & bc) { if (a == 0) { ac = 0; bc = 1; return b; } int g = egcd(b % a, a, bc, ac); ac -= (b / a) * bc; return g; }
int arr[100], sarr[100]; map<int,int> inv; int main() { for (int i = 0; i < 100; i++) { int a; cin >> a; arr[i] = sarr[i] = a; } sort(sarr, sarr + 100); fori (i, 0, 100) inv[sarr[i]] = i; fori (i, 0, 100) { cout << arr[i] << " " << inv[arr[i]] << endl; } }
set<int> s; int main() { for (int i = 0; i < 100; i++) { int a; cin >> a; if (s.count(a)) { cout << a << endl; } else { s.insert(a); } } }
(在比賽裡)queue用deque模擬就好了,其實想要的是priority_queue
priority_queue<int,vector<int>,greater<int> > q; // 傳greater表示希望top要是最小的。 // 邏輯:不傳的結果是less,平常的priority(優先序)要最大的先做。 int main() { q.push(1); for (int i = 0; i < 10000; i++) { int x = q.top(); q.pop(); q.push(x * 2); if (x % 2 == 0) q.push(x * 3); } printf("%d\n", q.top()); }
cout.precision(15); cout << setprecision(15) << fixed;
千萬不要在競賽或練習以外的場合寫這種鬼東西
#define fori(i,s,e) for (int i = (s); i < ((int)e); i++) // ↑↑↑ 其他常見寫法:FOR, REP #define allof(s) (s).begin(), (s).end() // ↑↑↑ 其他常見寫法:ALL #define scan_d(x) scanf("%d",&(x)) #define scan_dd(x,y) scanf("%d%d",&(x),&(y)) vector<int> v; int main() { fori (i, 0, 100) { int x; scan_d(x); v.push_back(i + x); } sort(allof(v)); }
// #define NDEBUG // ↑↑↑ 也影響到<cassert> #ifdef NDEBUG #define debugf(...) ((void)0) #else #define debugf(...) fprintf(stderr, __VA_ARGS__) #endif
// 也適用於int,不過純粹拿來做位元運算的數通常用unsigned: unsigned int a; // 0 ~ 2^32 - 1 unsigned long long b; // 0 ~ 2^64 - 1 cin >> a >> b; cout << (~a) << endl; cout << (a & b) << endl; cout << (a | b) << endl; cout << (a ^ b) << endl; // 不是冪哦 cout << (a << b) << endl; // 跟cout的意義完全不相干 cout << (a >> b) << endl; // 跟cin的意義完全不相干
C留下來的。通常可以用reference的時候就盡量用reference(不會莫名奇妙爆掉),不過如果要寫複雜的資料結構(treap!!!)就不可避免
struct Node { int key; Node * lc; Node * rc; Node(int k): key(k), lc(NULL), rc(NULL) {} Node * find(int k) { if (k == key) return this; if (k < key) return (lc == NULL) ? NULL : lc->find(k); return (rc == NULL) ? NULL : rc->find(k); } };
vector<int> v; int main() { // ... 輸入到v之類的 ... for (vector<int>::iterator it = v.begin(); it != v.end(); it++) { *it *= 2; } // 下面要編譯器夠新! for (int & x : it) { x *= 2; } for (auto & x : it) { x *= 2; } } // map<int,char>::iterator it // pair<int,char> p = *it
要編譯器夠新!
int main() { int x, y, z; auto rotate = [&](){ int temp = z; y = x; z = y; x = temp; }; cin >> x >> y >> z; cout << x; rotate(); cout << x; rotate(); cout << x; }
真的很少用了,大概只有時限超緊的位元枚舉
unsigned int a; cin >> a; cout << __builtin_clz(a) << endl; cout << __builtin_ctz(a) << endl; cout << __builtin_popcount(a) << endl; // __builtin_popcountl(unsigned long) // __builtin_popcountll(unsigned long long)
讀入兩個整數(絕對值不會「很大」),輸出這兩個數字之和
#include <iostream> using namespace std; int main() { int a, b; while (cin >> a >> b) cout << a + b << "\n"; } }
#include <cstdio> using namespace std; int main() { int a, b; while (scanf("%d %d", &a, &b) != EOF) { printf("%d\n", a + b); } }
Zerojudge b209。請自己閱讀題目。還沒有AC過競賽題目的人可以先寫它。
(加一點點數學底子)
基本的逃不掉。
動畫支援~VisualSort
先掃描一次,「選擇」最小的元素移到最左邊
再掃描一次,「選擇」第二小的元素移到左邊
再掃描一次,「選擇」第三小的元素移到左邊
⋯⋯
每次掃描,「選擇」剩下元素最小的移到左邊
(如何選擇最小的元素?)
for (int i = 0; i < length; i++) { int minIndex = i; for (int j = i + 1; j < length; j++) { if (a[j] < a[minIndex]) minIndex = j; } if (minIndex != i) { int tmp = a[i]; a[i] = a[minIndex]; a[minIndex] = t; // swap(a[i], a[minIndex]); in <algorithm> } }
第一個元素不用排序
第二個元素放到第一個元素前面或後面
第三個元素放到前兩個元素前面或中間或後面
第四個元素放到前三個元素某處
⋯⋯
左邊有一串已排序好,每次把右邊的下一個數字「插入」左邊的正確位置
Divide & Conquer 分而治之
排序左半,排序右半,合併!
★ VisualSort的動畫其實不準,正統的Merge sort需要額外記憶體暫存(不然合併的時候會一直動到中間的元素,幾乎跟Insertion Sort一樣差)
排序演算法還有很多。有實際用途的包括:
沒有實際用途的包括:
不過幾乎所有競賽中都輪不到自己寫~
std::sort(v.begin(), v.end());
另外你也可以自訂排序順序,一種方法是在struct定義operator<,另一種是傳函數給std::sort:
bool cmp(int a, int b) { // a要在b前面嗎? return a > b; }
std::sort(v.begin(), v.end(), cmp);
程式跑得夠快很重要(不然會TLE!)
不過怎麼知道「多快」呢?
$ time ./fairphoto real 0m0.771s user 0m0.763s sys 0m0.005s $ time ./fairphoto real 0m0.765s user 0m0.758s sys 0m0.005s $ time ./fairphoto real 0m0.781s user 0m0.773s sys 0m0.006s $ time ./fairphoto real 0m0.778s user 0m0.771s sys 0m0.005s
不能只用跑的⋯⋯
for (int i = 0; i < length; i++) { int minIndex = i; for (int j = i + 1; j < length; j++) { if (a[j] < a[minIndex]) minIndex = j; } if (minIndex != i) { swap(a[i], a[minIndex]); } }
那一步會被跑最多次?
for (int i = 0; i < length; i++) { int minIndex = i; for (int j = i + 1; j < length; j++) { if (a[j] < a[minIndex]) minIndex = j; } if (minIndex != i) { swap(a[i], a[minIndex]); } }
檢查j < length會被執行n(n+1)/2次
j++會被執行n(n-1)/2次。
a[j] < a[minIndex]會被執行n(n-1)/2次。(n = length)
minIndex = j;會被執行0次~n(n-1)/2次。
100000e54: 44 8b 1d bd 01 00 00 mov 0x1bd(%rip),%r11d 100000e5b: 45 85 db test %r11d,%r11d 100000e5e: 7e 5a jle 100000eba <__Z7selsortv+0x6a> 100000e60: 41 b8 01 00 00 00 mov $0x1,%r8d 100000e66: 45 31 d2 xor %r10d,%r10d 100000e69: 48 8d 15 b0 01 00 00 lea 0x1b0(%rip),%rdx 100000e70: 4d 8d 4a 01 lea 0x1(%r10),%r9 100000e74: 45 39 d9 cmp %r11d,%r9d 100000e77: 4c 89 c1 mov %r8,%rcx 100000e7a: 44 89 d7 mov %r10d,%edi 100000e7d: 7d 3b jge 100000eba <__Z7selsortv+0x6a>
@_@
c1n(n+1)/2 + c2n(n-1)/2 + c3n + …
(c1/2 + c2/2) × n2 + …
O(n2)
f(x) ∈ O(g(x)) ⇔ ∃ M, x0 s.t. ∀ x > x0, |f(x)| ≤ M|g(x)|
(更嚴謹的話可以寫Big-Theta Notation: Θ(n2))
通常常數不會太大,演算法要跑完的重點是當n很大的時候造成的時間成長
目標大概是「107件事情」
Codeforces 207 Div 1 D: Bags and Coins
不用位元運算硬壓到常數÷32就會
不過這種題目很少
花的時間叫做T(n)
排序左半 → T(n/2)
排序右半 → T(n/2)
合併:O(n)
T(n) = 2T(n/2) + kn
???
O(n log n),也是一般*排序最好的速度
嚴謹/一般做法,參見Master Theorem
其實競賽中幾乎永遠都是Modular exponentiation,因為大數又難寫又把複雜度搞得亂七八糟
「請輸出答案mod 109 + 7」(是質數)
10123456789 mod 109 + 7
long long p = 1; for (int i = 0; i < 123456789; i++) { p = (10 * p) % 1000000007; }
10123456789 = 10123456788 × 10 mod 109 + 7
10123456789 = 10123456787 × 10 × 10 mod 109 + 7
10123456789 = 10123456786 × 10 × 10 × 10 mod 109 + 7
如果是競賽這樣算大概會TLE,不過自己跑應該還是跑得出來
挑戰計算:21016 mod 1017 + 3
Hint: 10123456789 = 1061728394 × 1061728394 × 10
對不起我錯了
小心的話,在蠻大的mod下面乘兩個long long還是做得到的啦:
long long mmul(long long a, long long b) { long long res = 0; while (a) { if (a % 2 == 1) res = (res + b) % MOD; a /= 2; b = (b * 2) % MOD; } return res; }
會做了嗎?
並非單一演算法,而是一套想法,可以發展很多演算法。
把一個問題切成小塊。
重複子問題overlapping subproblems
最佳子結構optimal substructure
Fibonacci
int fibo(int ix) { // F(0) = 0, F(1) = 1 if (ix == 0 || ix == 1) return ix; return fibo(ix - 1) + fibo(ix - 2); }
跑跑看?
fibo(10)
fibo(9) + fibo(8)
fibo(8) + fibo(7) + fibo(8)
fibo(8) + fibo(7) + fibo(8)Overlapping Subproblems!
Fibonacci: Bottom-Up
int fibo[50]; void init() { fibo[0] = 0; fibo[1] = 1; for (int i = 2; i < 50; i++) fibo[i] = fibo[i-1] + fibo[i-2]; }
Fibonacci: Top-Down (Memoization)
int _fibo[50]; void init() { for (int i = 0; i < 50; i++) _fibo[i] = -1; } int fibo(int ix) { if (_fibo[ix] != -1) return _fibo[ix]; if (ix == 0 || ix == 1) return _fibo[ix] = ix; return _fibo[ix] = fibo(ix - 1) + fibo(ix - 2); }
問題是「如何決定什麼是子問題?」
要求:
經驗
Longest Common Subsequence
"dynamicprogramming"
"dynamicprogramming" → "program"
"dynamicprogramming" → "ioi"
"dynamicprogramming" → ""
"dynamicprogramming" → "dynamicprogramming"
"dynamicprogramming" vs "algorithms"
"dynamicprogramming" vs "algorithms" → "agri"
哪裡有子問題?
"dynamicprogramming" vs "algorithms"
有用嗎?為什麼?
如果紅色的字母是同一個呢?
for (int i = 1; i <= an; i++) { for (int j = 1; j <= bn; j++) { dp[i][j] = max(dp[i-1][j], dp[i][j-1]); if (a[i-1] == b[j-1]) dp[i][j] = max(dp[i][j], 1 + dp[i-1][j-1]); printf("%d ", dp[i][j]); } printf("\n"); }
total 100kg → $?
14kg $25 21kg $36 39kg $60 60kg $82 82kg $99有三種版本:01,無限,Fractional
第三種不需要DP(為什麼?)
全部檢查?O(2n)
其他子問題?
14kg $25 21kg $36 39kg $60 60kg $82 82kg $99100kg → $? | 86kg → $?
| 72kg → $? | 58kg → $? | 44kg → $? | 30kg → $? | 16kg → $? | 2kg → $?
Codeforces R#260 Div1A, RCC 2014 Div2A, R#240 Div1B