演算法初探

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

TIOJ  建中資訊社的 Judge

ZeroJudge  台灣最多人用的OJ

Codeforces 最大的 OJ/線上競賽平台

Atcoder  次大的 OJ/線上競賽平台

CSES 經典題大全

CSAcademy 解題技巧大全

比賽

除了台灣的實體比賽外,還有許多線上競賽可以打

 

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;
O(\sqrt n)

以下程式的時間複雜度是...?

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;
O(n^2)

以下程式的時間複雜度是...?

O(\log n)
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;
O(n)

計算時間複雜度往往不是數幾個迴圈那麼顯然

需要依靠其他的性質才能正確算出!

時間複雜度與執行時間

我這個寫法到底夠不夠快?

\(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\)

例題 - 田忌賽馬

例題 - 隕石

例題 - 魚FISH

遞迴

函式的概念

數學裡的函式,一般會用 \(f(x) = y\) 表示

其中 \(f\) 稱為函數/函式

\(x\) 是輸入值

\(y\) 是輸出值

 

ex. \(f(x) = 71x + 22, f(a, b) = 3a^3 - b^2\)

在C++, 函式除了可以回傳值,還可以在裡面改變其他變數,把一段程式碼包成一個「動作」

遞迴?

當一個函式的定義用到它自己的時候

f(x) = \left\{ \begin{array}{ c l } 2*f(x-1) & \quad \textrm{if } x \geq 1 \\ 1 & \quad \textrm{otherwise} \end{array} \right.

上述函式輸入一個非負整數\(x\),輸出\(2^x\)

遞迴的寫法

  • ​邊界條件/初始值

  • 遞迴式

 

遞迴的想法

  • 把問題拆成大問題跟小問題

  • 每個問題都有固定的做法
  • 終止條件

例: 輾轉相除法

gcd(a, b) = \left\{ \begin{array}{ c l } b & \quad \textrm{if } a = 0 \\ gcd(b \% a, a) & \quad \textrm{otherwise} \end{array} \right.
  • ​邊界條件/初始值

  • 遞迴式

 

例: 河內塔

把左邊的每個環移到中間,一次只能搬一個環,而且上面的環一定要比下面小。

例: 八皇后問題

有一個\(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;

STL

Algorithm

Vector

Utility (pair)

String

stack/queue

set, map

bitset

資訊競賽概論 (校培)

By justinlai2003

資訊競賽概論 (校培)

  • 3,224