
時間複雜度
講師 蕭梓宏

極粗略課程大綱
-
甚麼是時間複雜度?
-
時間複雜度對我(你)來說很重要嗎?
-
怎麼估複雜度?

針對這堂課的提醒
-
雖然是遠距上課,但還是希望大家能有多一點互動
-
有想法就可以提出來
-
-
課程內容的難易度未必適合所有人
-
有問題就問
-
覺得太簡單不用急,後面的講師一定可以滿足你
-
-
雖然這堂課可能偏理論,但並非那麼嚴謹
什麼是時間複雜度?

某個演算法的時間複雜度?
-
一個描述該演算法(或程式)執行時間的函式
-
通常假設各種基本操作都花一單位時間
-
加、減、乘、除、取模
-
賦值
-
條件判斷
-
存取變數
-
…...
-

舉例
F(N)=N2+2N+3
cin >> N;
i = 0;//1
j = 0;//1
while(i < N)//N+1
{
while(j < i)//1+2+3+..+N=(N+1)N/2
{
j++;//1+2+3+..+(N-1)=(N-1)N/2
}
i++;//N
}
這個算起來也太麻煩了吧

big-O
-
f(x)=O(g(x)) 代表存在M,x0>0,使得所有x≥x0都有∣f(x)∣≤M∣g(x)∣
-
也就是說,當x很大的時候,存在常數M,使得M∣g(x)∣一定會大於等於f(x)
-
其實也就表示,g(x)是f(x)的某種上界
-
看例子!

例子
f(x)
g(x)
f(x)=O(g(x))?
x
100x
Yes
x
100x
Yes
x3
x2
No
x2
10000x2log(x)
Yes
Yes
x−1000x−100
x

big-O
-
可以看出,只要有了big-O符號,我們就可以無視低次項跟常數,更方便地表示複雜度
-
因此,在剛剛的例子中,原本複雜度是
F(N)=N2+N+4,現在也可以用O(N2)來表示該程式的時間複雜度 -
雖然也可以用O(N3)來表示,但我們追求的是複雜度越小越好!
-
這樣估複雜度就不用那麼麻煩了!

小測驗
O(N3)
cin >> N;
i = 0;
j = 0;
while(i < N*N+N-2)
{
while(j < N*3-10)
{
j++;
}
i++;
}

小測驗
O(N2N)
cin >> N;
i = 0;
j = 0;
while(i < N*N+N-2)
{
while(j*j < N)
{
j++;
}
i++;
}
時間複雜度很重要嗎?

很重要
-
即使你的程式是正確的,如果他的執行時間超過了題目的限制,你會得到一個 而無法得到分數
-
一般的judge一秒可以跑108筆運算左右,所以想要知道自己的程式會跑幾秒,就把題目的範圍限制代入所估的複雜度中,最後再除以108。然後再跟題目的限制秒數比比看就可以知道會不會TLE了
-
注意我們要估的通常是最糟的情形


第一個例題!
給定一個單字,請你找出其中"最長的"連續的一段,使得那段正著唸反著唸都一樣。(單字長度L≤5000)
範例輸入:
abcbbd
範例輸出:
bcb

作法其一
枚舉所有的「連續的一段」,再把那一段掃過一遍就好囉!
abac
abac
abac
abac
abac
abac
abac
abac
abac
abac

作法其一 複雜度
因為長度為1的區間有L個,2的區間有L−1個,...,L的區間有1個,所以複雜度是1×L+2×(L−1)+...+L×1=6L3+3L2−4L
複雜度是:
枚舉連續的一段 O(L2)
然後再把那段掃一遍 最糟O(L)
所以就是O(L3)囉!

這樣OK嗎?
- 已知L≤5000,而複雜度為L3
- 50003=1.25×1011>>108
跑100秒都跑不完QQ

作法其二
枚舉所有的「中間點」,然後看可以往外擴張多少!

作法其二
枚舉所有的「中間點」,然後看可以往外擴張多少!
a b c b b d
長度:3

作法其二
枚舉所有的「中間點」,然後看可以往外擴張多少!
a b c b b d
長度:2

作法其二 複雜度
複雜度是:
枚舉中間點(包含中間字母語中間空隙) O(L)
然後看可以往外擴張多少 最糟O(L)
所以就是O(L2)囉!
50002=2.5×107,理論上一秒內可以跑完!

第一個例題 實作時間
回文好玩好文回(6)
10 min

複雜度真的很重要
-
除了判斷自己的演算法可不可以拿到想要的分數之外,因為比較難的題目通常都會把限制的範圍出的盡量大,所以看到範圍後也可以得到一些關於作法複雜度的線索

第二個例題!
給你一個正整數Q,接下來有Q筆詢問,每次詢問你一個整數N是不是質數。
範例輸入:
3
1
2
3
範例輸出:
No
Yes
Yes

第二個例題! 版本一
給你一個正整數Q,接下來有Q筆詢問,每次詢問你一個整數N是不是質數。
範圍:Q≤100,N≤1010
第二個例題! 版本二
給你一個正整數Q,接下來有Q筆詢問,每次詢問你一個整數N是不是質數。
範圍:Q≤106,N≤106

版本一 Q≤100,N≤1010
- 這個版本的Q範圍比第二個版本小,所以我們每筆詢問的複雜度把N帶入後不超過100108=106就好,所以每一筆詢問可以相對暴力地回答
說到最直接判斷質數的方法
試除法?

試除法 複雜度
- 跟我們平常用手算一樣,試除法有一個重點是如果N是合數,他至少有一個因數d≤N
- 換句話說,我們只要拿2,3,...,⌊N⌋這O(N)個東西去除除看就可以確認他是不是質數了
- 對每筆詢問都做一樣的事,複雜度O(QN)
- Q≤100,N≤1010帶進去算出來大概是107
- 讚讚!

第二個例題 版本一 實做時間
質數判斷(7)
9 min

版本二 Q≤106,N≤106
- 直接用試除法,那個複雜度應該是過不了的!
- 相對於上一題,這一題的突破口是N比較小
- 其實,我們只要知道1~106中每個數是不是質數就好了!
說到找出比K小的質數的方法
篩法?

篩法
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
這樣做的複雜度是2K+3K+5K...
有點難估所以抓個上界:
2K+3K+5K...≤K(1+21+31+...+K1)

篩法 複雜度
- 可以學(記)起來的小引理:1+21+31+...+K1=O(log(K))
- 所以複雜度就是2K+3K+5K...≤K(1+21+31+...+K1)=O(Klog(K))
- 所以我們一開始先用O(Klog(K))的時間建好K=106以下的質數表,並存在陣列中
- 然後每筆詢問N直接O(1)去看陣列中第N格
- 總複雜度O(Klog(K)+Q);K=106,Q≤106 讚讚!

第二個例題 版本二 實作時間
質數判斷2(11)
9 min

第二個例題!
- 所以,除了題目本身之外,也要根據範圍來選擇適合的做法!
Q≤100,N≤1010
Q≤106,N≤106
O(QN)
O(Klog(K)+Q)
AC
AC
TLE
TLE

有此一說(僅供參考)
- 如果有一題他的複雜度只跟N有關的話
範圍
做法複雜度
N≤10
O(4N),O(N7),O(N!),...
N≤20
O(N×2N),O(N6),...
N≤50~ 100
O(N4)
N≤200
O(N3log(N))
N≤500
O(N3)
N≤2000
O(N2log(N))

有此一說(僅供參考)
- 如果有一題他的複雜度只跟N有關的話
範圍
作法複雜度
N≤5000
O(N2)
N≤5×104
O(NN)
N≤105
O(NN),O(Nlog2(N)),O(Nlog(N))
N≤106
O(Nlog(N)),O(N)
N≤107
O(N)
N≤1012
O(N)
N≤1018
O(log(N)),O(log2(N)),...
怎麼估時間複雜度?

估時間複雜度的方法
-
其實大家都會了?
-
根據之前的例子,基本上就是看每一層迴圈最多跑多少次,然後全部乘起來?
但是......

第三個例題!
給你一個單字,你要從中找到其中連續的一段,使得該段是包含全部的字母中最短的(很多個的話請輸出最先出現的,不存在請輸出"QQ")
範例輸入:
aabbabcdefghijklmnoqrstuvwxyz
範例輸出:
abcdefghijklmnoqrstuvwxyz

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
1 |
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
1 | 1 |
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
1 | 1 | 1 |
L
長度:3

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
1 |
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
1 | 1 |
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
2 | 1 |
L

某個作法
- 枚舉「連續的一段」的左界L, 開一個26格的陣列代表每個字母出現過幾次,然後從L往右一直看直到包含所有字母為止,並記錄當前長度
- 假設只有三個字母(a,b,c)
a b c b a
a | b | c |
---|---|---|
1 | 2 | 1 |
L
長度:4

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
L

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
1 |
L

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
1 | 1 |
L

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
1 | 1 | 1 |
L
長度:3

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
1 | 1 |
L

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
2 | 1 |
L

某個作法 分析
- 複雜度應該蠻容易可以看出是枚舉L的O(N)乘上往右掃的O(N),所以是O(N2)
- 但是應該也有人發現其實L往右一格之後,不用從新的L開始往右掃,只要把原本L的字母出現次數減一,然後繼續掃就好
a b c b a
a | b | c |
---|---|---|
1 | 2 | 1 |
L
長度:4

某個改進的作法
- 也就是說
for(L = 1;L <= N;L++)
{
for(R = L;R <= N;R++)
{
//do something
if(something happens)
{
ans = min(ans,R-L+1);
break;
}
}
}
int R = 1;
for(L = 1;L <= N;L++)
{
while(R <= N)
{
//do something
if(something happens)
{
ans = min(ans,R-L+1);
//do something
break;
}
R++;
}
}

某個改進的作法 分析
- 如果跟之前一樣看每個迴圈最多跑多少再乘起來的話,複雜度還是O(N2)
- 明明感覺變快不少?

某個改進的作法 分析
- 我們之所以把複雜想成O(N2),是因為我們覺得中間的while迴圈最慘會執行那麼多次
- 但其實可以發現,中間的while迴圈每跑一次,R就會加一,但是只有R≤N的時候while迴圈才會執行,所以while迴圈只會執行O(N)次!
- 所以總複雜度其實是O(N)!

第三個例題 實作時間
電皇的小寫英文字母(5)
15 min

均攤分析
- 有時候某個迴圈跑的數量我們不確定,就只能以最糟情況來計算複雜度
- 但或許我們可以想想,他總共跑的次數或許會受到某種限制
- 這種分析方式稱為均攤分析,通常要正確分析會需要一些經驗
- 剛剛例題中的演算法被稱為「雙指針」或「爬行法」,是很經典可以透過均攤分析找到真正複雜度的演算法

遞迴函式 複雜度分析
- 如果是遞迴函式,就不能把迴圈直接乘起來囉!
- 但是遞迴函式也有各式各樣的,這邊就只先舉簡單的幾個例子
- 主定理甚麼的聽說分治那堂課會教?

送分題
int f(int n)
{
if(n == 1) return 1;
return n*f(n-1);
}
f(n)→f(n−1)→...→f(1)
每個函式本身都O(1)
總共O(n)!

快速冪
int Pow(int a,int b)
{
if(n == 0) return 1;
int t = Pow(a,b/2);
if(b & 2 == 1) return t*t*a;
else return t*t;
}
每個函式本身都O(1),共有O(log(b))次函式被呼叫
總共O(log(b))!


分治預習
void f(int n)
{
if(n <= 1) return 1;
//Do some O(n) things
f(n/2);
f(n/2);
}
f(n)
f(⌊2n⌋)×2
f(⌊4n⌋)×4
⋯
⋯
⋯
f(1)×n
每層都是跑O(n)
總共有O(log(n))
總複雜度O(nlog(n))!

準備下課啦
- 除了時間複雜度之外,也有空間複雜度
- 除了時間複雜度之外,還有常數
- 這堂課應該比接下來的課簡單,但很重要
- 接下來的課可能很大的比例就是在教怎麼樣才能讓複雜度降低
- 接下來的課一定會更有趣,講師也更強,請好好期待><

題目小整理
上課例題:
- 6,7,11,5
回家練習:
- 8,9,10
謝謝大家!
APCS Camp時間複雜度
By Zi-Hong Xiao
APCS Camp時間複雜度
APCS Camp 資料結構
- 820