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
[1] != [1]
, [1] == 1
!?
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寫個網站好了
Vim | Git / GitHub |
---|---|
©® GitHub 2014 |
© Joshua Schulter under GPL
當然做其他事情也需要學演算法,但更會遇到很多架構/設計的問題,或是需要學習用各種模組跟API,或是debug。
(Knuth, The Art of Computer Programming)
Source: Wikicommons
這是演算法嗎?
可以解決的問題:
gcd(a, b) = ?
「= 0」、「mod」是有限、明確、有效的。
在GCD裡面再計算一次GCD是明確、有效的(做同樣的判斷、計算,直到有答案為止)。
是有限的(為什麼?)
a | b |
---|---|
39 | 15 |
39 mod 15 = 9 | |
15 | 9 |
15 mod 9 = 6 | |
9 | 6 |
9 mod 6 = 3 | |
6 | 3 |
6 mod 3 = 0 | |
3 | 0 |
gcd(39, 15) = … = gcd(3, 0) = 3 |
線上賽似乎都很殘忍的只回傳部分訊息
為信仰而戰!
遍、地、開、WA!
— rilak
學過的先做一下題目吧。例如挑戰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;
}
}
初階可以先跳過;大約照重要性跟實用性遞減排序。
我寫的有點過火,不用看完
?:
<algorithm>
, <vector>
<cstdio>
?:
可以當成一個值或算式的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";
}
<algorithm>
, <vector>
#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()); // !!!
}
<cstdio>
雖然cin, cout用起來很順手,不過比賽用它遲早會被雷:
速度慢!
#include <cstdio>
using namespace std;
int main() {
int a, b;
scanf("%d%d", &a, &b);
printf("%d%d", a, b);
}
C留下來的函數,所以傳入scanf
的變數需要用&
(包成指標)。
%d
= int%lld
OR %I64d
(看OS) = long long<cstdio>
不過如果要輸出浮點數的時候⋯⋯
#include <cstdio>
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double x;
printf("%.15lf\n", x);
cout << fixed << setprecision(15) << x << endl;
}
<cstdio>
對了,還有字串輸入輸出
字串結尾有一個\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;
}
<map>
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>
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>
(在比賽裡)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());
}
<deque>
, <list>
<queue>
→ priority_queue
<set>
→ multiset
<map>
→ multimap
<utility>
→ pair
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)
讀入兩個整數(絕對值不會「很大」),輸出這兩個數字之和
iostream
version
#include <iostream>
using namespace std;
int main() {
int a, b;
while (cin >> a >> b)
cout << a + b << "\n";
}
}
cstdio
version
#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件事情」
O | n? |
---|---|
O(log n) | 10100+ |
O(n) | 106 ~ 107 |
O(n log n) | 105 ~ 3 × 105 |
O(n2) | 103 ~ 3 × 103 |
O(n3) | 100 |
O(2n) | 20 ~ 25 |
O(n!) | 8 ~ 10 |
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
???
8 | |||||||
4 | 4 | ||||||
2 | 2 | 2 | 2 | ||||
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
13 | ||||||||||||
7 | 6 | |||||||||||
4 | 3 | 3 | 3 | |||||||||
2 | 2 | 2 | 1 | 2 | 1 | 2 | 1 | |||||
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
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 | $99 |
100kg → $? | 86kg → $?
| 72kg → $? | 58kg → $? | 44kg → $? | 30kg → $? | 16kg → $? | 2kg → $?
Codeforces R#260 Div1A, RCC 2014 Div2A, R#240 Div1B