演算法初探
9/15 資訊校隊培訓
建國中學 賴昭勳
講師介紹
賴昭勳 aka 8e7
- 高一 TOI 1!, 高二 TOI 2!
- 北市賽一等獎 / 全國賽佳作 ;-;
- APIO 銀牌
- CF: 8e7, Atcoder: justinlai
- 很弱
- 不會數學
- 正在慢慢變油
- 為了初探演算法所以當演算法初探講師
王褕立 aka FHVirus
- 高二 TOI 1!
- 北市賽二等獎 / 全國賽三等獎
- CF: fhvirus
- 很強
- TIOJ Top Topcoder
- 會FFT跟Flow
- 超油,喜歡看冷門番
- 魔方社社長,3x3 PB 8秒多
張均豪 aka Jass
- 高一、高二 TOI 1!
- 北市賽一等獎 / 全國賽三等獎
- CF: jass921026
- 很強(科班電神)
- 很油
- osu! std 30k 大佬
所以校隊要做什麼?
比賽時程 (上)
校內複賽 | 10/5 | 選校隊 11(?)+3人 |
北市賽 | 11/15前 | 前二等獎進全國 (11人) |
全國賽 | 12月 | 前二等進選訓 (10人) |
比賽時程 (下)
TOI 入營考 | 3月初 | 選20個進一階 (+女保) |
一階 | 約 3/15~3/28 | 模考前12名進二階 |
二階 | 4月 | 選4名國手 |
IOI 2022 | 7月 (?) 在印尼 | 台灣加油w |
其他比賽
YTP | 10/9 | 好吃 前十多組晉級專題階段(賺爛了賺爛了) |
ISSC | 10/16 | 一校最多兩隊* 台中比賽,題目品質極不穩但有錢拿 |
HP Codewars | 11月 | 有一堆獎品可以拿 線上賽的話就很無聊QQ |
NPSC | 12月 | 初賽取25隊/一校最多三隊,好題,賺 |
三人團體賽
啊所以打競程可以幹嘛
除了拚升學上資工之外...
我也不知道,很酷就對了 (x)
希望大家能把寫程式當作自己的興趣
漸漸地探索解題的有趣之處!
當然,如果你覺得自己不適合的話也沒有關係!
要怎麼練習?
學習
2016 建中校培講義 (初學者1~7章就夠了)
演算法筆記 (解釋清楚,實作有點糟)
CP-Algorithms (很好懂,英文)
USACO Guide (進階)
2021 建中校培講義(?)
另外,可以參加每週二晚上的資訊讀書會!
寫題
使用 Online Judge
比賽
除了台灣的實體比賽外,還有許多線上競賽可以打
Codeforces 通常在 22:35~00:35
Atcoder 周末 20:00~22:00
Google Codejam - 年度
Facebook Hacker Cup - 年度,深夜場,怪賽制
剛開始的時候,可以從上課或是講義內容先找主題,理解概念之後再寫例題。
基本的東西學的差不多之後,可以參加沒有範圍線上比賽,或是戳任意主題的題目,訓練自己靈活運用演算法的能力。
競程裡面的演算法?
演算法: 解決問題的方法
問題: 可以被客觀定義,描述的問題。
方法: 可以被客觀描述的流程與步驟。大部分演算法應該對同一種類的問題都適用。
例: 給定兩個正整數 \(a, b\),輸出\(a, b\)的最大公因數
一個好的演算法應該能夠對於任意範圍內的\(a, b\)輸出正確答案。
在競程裡面
問題和答案通常以純文字形式輸入,或是用數字,字串等格式溝通。
Online Judge 判斷回答正確的方法為,使用一些測試資料 (測資) 來檢測程式的輸出是否符合條件。
正確解答必須在規定的時間限制 (TL) 和記憶體限制 (ML) 執行出正確答案。
複雜度分析
估算執行時間的方法
哪個程式跑比較快?
const int maxn = 100005;
int a[maxn];
for (int i = 0;i < n;i++) cin >> a[i];
int ans = 0;
for (int i = 0;i < n;i++) {
for (int j = 0;j < n;j++) {
ans = max(ans, a[i] - a[j]);
}
}
cout << ans << endl;
const int maxn = 100005;
int a[maxn];
for (int i = 0;i < n;i++) cin >> a[i];
int ma = 0, mi = 0;
for (int i = 0;i < n;i++) {
ma = max(ma, a[i]);
mi = min(mi, a[i]);
}
cout << ma - mi << endl;
問題:
給定一個整數序列 \(a\)
求出相差最大的兩項之差
\(max_{1 \leq i, j \leq n} |a_i - a_j|\)
Why?
假設迴圈內每個指令都花一樣時間執行...
n (序列長度) | 程式 1 (兩層 for) | 程式 2 (一層 for) |
---|---|---|
3 | 9 | 6 |
1000 | 1000000 | 2000 |
100000 | 10000000000 | 200000 |
當\(n\)的值很大的時候,第二個程式的時間跟\(n\)差不多,第一個程式卻慢了很多...
時間複雜度: 計算次數隨問題大小增長的方式
第一個程式執行 \(n^2\)次操作,第二個程式執行了\(2n\)次操作。
我們分別用 \(O(n^2)\)和 \(O(n)\)來表示兩個程式的時間複雜度
時間複雜度: 嚴謹定義
定義一個問題在大小\(n\)時的執行次數為\(f(n)\)
一個演算法的時間複雜度為 \(f(n) \in O(g(n))\)
代表存在兩個正整數 \(M, n_0\)使得
\(\forall n \geq n_0, f(n) \leq Mg(n)\)
通常我們在乎的是符合上述條件中,成長速度最慢的函數\(g(n)\)
空間複雜度也是一樣的道理
考慮的是一個問題使用的記憶體空間
時間複雜度的性質和運算
成長速度: \(指數 > 多項式 > 底數 > 常數\)
忽略\(g(n)\)的常數項: \(O(5x^2) \rightarrow O(x^2)\)
多個函數相加時,取成長速度最快的一項:
\(O(g_1) + O(g_2) = O(max(g_1, g_2))\)
\(O(g_1) * O(g_2) = O(g_1g_2)\)
練習時間!
以下程式的時間複雜度是...?
int n;
cin >> n;
bool isprime = 1;
for (int i = 2;i*i <= n;i++) {
if (n % i == 0) isprime = 0;
}
cout << isprime << endl;
以下程式的時間複雜度是...?
int n;
cin >> n;
int a[n], pref[n];
for (int i = 0;i < n;i++) {
cin >> a[i];
pref[i] = a[i];
if (i > 0) pref[i] += pref[i-1];
}
int ans = 0;
for (int i = 0;i < n;i++) {
for (int j = 0;j < i;j++) {
ans = max(ans, pref[i] - pref[j]);
}
}
cout << ans << endl;
以下程式的時間複雜度是...?
int n;
cin >> n;
int cnt = 0;
while (n > 0) {
cnt++;
n /= 2;
}
cout << cnt << endl;
以下程式的時間複雜度是...?
int n, k;
cin >> n >> k;
int a[n], pref[n];
for (int i = 0;i < n;i++) {
cin >> a[i];
pref[i] = a[i];
if (i > 0) pref[i] += pref[i-1];
}
int ans = 0, ind = 0;
for (int i = 0;i < n;i++) {
while (ind <= i && pref[i] - pref[ind] + a[ind] > k) {
ind++;
}
ans += i - ind + 1;
}
cout << ans << endl;
計算時間複雜度往往不是數幾個迴圈那麼顯然
需要依靠其他的性質才能正確算出!
時間複雜度與執行時間
我這個寫法到底夠不夠快?
\(n\)的大小
\(10\)
\(20\)~\(24\)
\(100\)~\(500\)
\(1000\)~\(5000\)
\(10000\)~\(10^5\)
\(10^5\)~\(10^6\)
\(\geq 10^6\)
對應的複雜度
\(O(n!)\)
\(O(2^n), O(n*2^n)\)
\(O(n^3logn), O(n^3)\)
\(O(n^2logn), O(n^2)\)
\(O(n\sqrt n)\)
\(O(nlogn)\)
\(O(n)\)
電腦一秒可以跑\(3*10^8\)個基本運算 (+, -, *, =)等。
左邊僅為參考用w
*假設時限 1 秒
二分搜尋法
來玩個遊戲
現在我心裡想一個 \(1\)~\(10000\)的數字,你每次可以猜一個數字,而我會告訴你猜的數字是\(\geq\)還是\(<\)心裡想的數字。能在幾次之內猜到呢?
資訊社員: 怎麼有點似曾相識...
縮小答案的範圍
-> 詢問的數字
回答為 \(\geq\)
回答為 \(<\)
注意到兩條黑色的線段不相交!
如果我們詢問的數字在答案區間的中間的話
那就可以把答案範圍平分成兩塊!
複雜度?
當答案區間只有一個數字時,我們就知道答案了,因此問題相當於最多要切多少次才能得到長度 1 的區間。
大約是 \(O(\log n)\)次,其中\(n\)是一開始的區間長度。
再看一題
給你 \(n\) 個整數的序列\(a\),請輸出第\(k\)小的數字
(不用 sort)
\(1 \leq k \leq n \leq 10^6, 1 \leq a_i \leq 10^3\)
解法
跟猜數字一樣,先猜一個答案\(x\),而我們想知道這個答案是\(\geq\)或\( < \)實際答案。
我們可以遍歷整個序列,數有幾個數字\(\leq x\)
假設有\(\geq k\)個數字,那\(x\)就大於等於實際答案,否則\(x\)就小於實際答案,藉此縮小範圍。
複雜度\(O(n\log C)\),其中\(C\)是答案範圍
單調性
剛剛的兩個題目能這樣做的原因,是一個叫單調性的性質。
前面問題可以看成:
在一個範圍內,尋找最小符合條件的數字
而單調性指的是: 存在一個數字\(x\)
使所有小於\(x\)的數字都不符合條件,
不小於\(x\)的數字都符合條件。
實作方式
int low = 0, up = 10001;
while (low < up - 1) {
int mid = (low + up) / 2;
if (condition(mid)) {
up = mid;
} else {
low = mid;
}
}
cout << up << "\n";
左開右開式: \(low\)必為 \(0\),\(up\)必為\(1\)
例題 - 田忌賽馬
例題 - 隕石
例題 - TOI 2021 初選pB
例題 - 魚FISH
遞迴
函式的概念
數學裡的函式,一般會用 \(f(x) = y\) 表示
其中 \(f\) 稱為函數/函式
\(x\) 是輸入值
\(y\) 是輸出值
ex. \(f(x) = 71x + 22, f(a, b) = 3a^3 - b^2\)
在C++, 函式除了可以回傳值,還可以在裡面改變其他變數,把一段程式碼包成一個「動作」
遞迴?
當一個函式的定義用到它自己的時候
上述函式輸入一個非負整數\(x\),輸出\(2^x\)
遞迴的寫法
-
邊界條件/初始值
-
遞迴式
遞迴的想法
-
把問題拆成大問題跟小問題
- 每個問題都有固定的做法
- 終止條件
例: 輾轉相除法
-
邊界條件/初始值
-
遞迴式
例: 河內塔
把左邊的每個環移到中間,一次只能搬一個環,而且上面的環一定要比下面小。
例: 八皇后問題
有一個\(8*8\)的棋盤,問有多少個方式放置八個皇后,使得任兩個皇后不互相攻擊。
一個皇后可以向她上下左右,以及斜向共八個方向任意距離直線攻擊。
實作技巧
位元運算
電腦裡面的數字都是用二進位表示,因此也有跟二進位相關的運算。
AND | & |
OR | | |
NOT | ~ |
XOR | ^ |
左移 | << |
右移 | >> |
暴力搜尋,枚舉
「我絕不使用暴力解決問題」- 開學宣誓詞
- 子集合?
- 排列?
- 組合?
可以使用遞迴減少實作難度!
雙指標
另一種單調性!
考慮這個問題: 給定一個正整數序列與整數\(k\),輸出有多少的序列的子區間內數字總和不超過\(k\)。
\(n \leq 10^6\)
觀察性質
一個子區間由他的左界和右界決定。
假設我們枚舉右界\(r\),對每個右界考慮最左邊的\(f(r) = l\)使得\([l, r]\)總和小於\(k\),
那麼就有\(r - l + 1\)的右界為\(r\)的區間符合條件。
那可以發現,隨著右界\(r\)變\(r+1\),對應的左界\(f(r)\)也會非嚴格遞減。因此我們要檢查的時候就可以直接從\(f(r)\)開始判斷!
int a[maxn];
for (int i = 0;i < n;i++) cin >> a[i];
int sum = 0, lef = 0;
ll ans = 0;
for (int i = 0;i < n;i++) {
sum += a[i];
while (lef <= i && sum > k) {
sum -= a[lef];
lef++;
}
ans += i - lef + 1;
}
cout << ans;
Algorithm
Vector
Utility (pair)
String
stack/queue
set, map
bitset
資訊競賽概論 (校培)
By justinlai2003
資訊競賽概論 (校培)
- 3,224