Chapter 0 競賽簡介(Competition in Informatics)
●Section 1 作戰計畫流程圖(Plan for Competition)
註:日期戰場數據依照每年實際情況而有所更動。
●Section 2 學習資源介紹(Introduction to Resourse)
§2-1 線上評測系統(Online Judge)
* Uva Onlinejudge(Uva):【推薦】
http://uva.onlinjudge.org/
* Usa Compute Olympiad(Usaco):【推薦】
http://ace.delos.com/
* Temporary INFOR Online Judge(TIOJ):【推薦】
http://tmtacm.no‐ip.org/JudgeOnline
* PKU Online Judge(PKUOJ):
http://acm.pku.edu.cn/JudgeOnline/
* Z‐Trening:
http://www.z‐trening.com/
* Velocious Informatics JudgeOnline System(Vijos):
http://www.vijos.cn/
* Timus Online Judge:
http://acm.timus.ru/
* Młodzieżowa Akademia Informatyczna:
http://main.edu.pl/
* Topcoder:
http://www.topcoder.com/
* Croatian Open Competition in Informatics:
http://www.hsin.hr/coci/
§2-2 書籍(Book)
★《Introduction to algorithm 3/e》:Thomas H. C. , Charles E. L. , Ronald L. R. , Clifford S.
★《Fundamentals of Data Structures in C(C++) 2/e》:Horowitz, Sahni, Mehta
★《算法藝術與信息學競賽》:劉汝佳、黃亮,清華大學出版社
其實要推薦的書有很多,但最經典最重要最常用的也就這三本書,如果對其他書有興趣,則可以另 外再找講師詢問。
§2-3 網路資源(Internet Resourse)
◎ 建中培訓網站:
http://www.ck.tp.edu.tw/~peng/
阿就我們培訓網站嘛 XDD。
◎ math120908 講師個人網站:
http://math120908.infor.org/
其實這些網址在我空間上都有,而講義應該也會放在這裡。
◎ Cppreference:
http://www.cppreference.com/
此網站說了一些基本的 C/C++函式介紹,查詢函式時非常好用。
◎ DJWS 的網路日誌:
http://djws.wordpress.com/
此網站有許多關於演算法的介紹。
◎ hil 教授的上課投影片:
http://www.csie.ntu.edu.tw/~hil/teach.html
此為台大演算法教授 hil 老師上課所使用的投影片。◎ BBS 上的 sa072686 個人版:
telnet://sony.twbbs.org/
的 sa072686 個人版 此有 sa072686 同學寫 Uva 一千多題的解題記錄。◎ Google、Wikipedia……。
~( ̄▽ ̄)~(_△_)~( ̄▽ ̄)~(_△_)~( ̄▽ ̄)~
Chapter 1 基礎演算法(Basic Algorithm)
●Section 1 複雜度分析(Complexity)
相信大家在解數學問題的時候,都常常會想用一些高級而快速的方法,減少計算式子,
不只是能求快,也比較不容易計算錯誤。
是的,身為一個好的演算法,就必須要滿足「快」「狠」「準」這些條件。唔!簡單的說,
就是要讓程式很快跑出答案,然後答案就是正確的啦 XD。
不過聽說現在電腦一秒鐘可以執行好幾億個指令!?那要怎麼比較演算法的效率是高還 低呢?畢竟在每台電腦跑的速度都不一樣,而且輸入的數量多寡是不固定的。嘿嘿,這時候,
我們就要引進一種,叫做時間複雜度(Time Complexity)的東西。
既然不能吃,那要怎麼用呢?直接舉個例子來說吧,就拿排序n個數的演算法來說好了,
現在有兩種演算法,第一種需要使用2n2個指令才能把這n個數依大小順序排好,第二種則
需要50 logn 2n個指令。又我們假設今天有一台電腦每秒可以跑108個指令。以下是時間對照
表:
n 102 103 104 105
第一種排序法(2n2) 0.2(ms) 20(ms) 2(s) 200(s) 第二種排序法(50 logn 2n) 0.33(ms) 4.98(ms) 0.066(s) 0.830(s)
我們可以發現到,在n很小的時候,兩種執行速度都快的不得了,甚至第一種還快於第 二種,但是,當n越來越大的時候,因為第一種演算法的成長速度比第二種快,所以,速度 快的優勢就被追過去了,然後就會變成很慢很慢……,由下圖可以看出兩者的成長趨勢。
所以了,我們要估計某個演算法的時間複雜度的時候,都是利用成長率最快的項來估計 的,因此,我們通常也會忽略常數,畢竟在當n很大,時間函數成長很快的時候,常數就顯 得微不足道了。舉個例子:如時間函數T n( ) 100 n314n7,影響這個函數成長的領導項是
n3,這個時候,我們會說他的時間複雜度是O T n( ( ))O n( )3 ,其中的O這個符號(唸作 Big-O)
是我們用來代表影響時間函數成長的最重要因子,一般來說,多項式時間的演算法,這項都 會是他的最高次項。
§1-1 漸進記號(Asymptotic Notation)
有了簡單的函數增長概念,我們就來嚴格定義一下以下幾個漸進記號。
# Θ(theta):對於一個給定的函數 g(n), 用Θ(g(n))來表示函數集合:
1 2 0 0 1 2
( ( )) { ( ) :g n f n c c n, , R, n n 0 c g n( ) f n( ) c g n( )}
# O(Big O):
0 0
( ( )) { ( ) : , , 0 ( ) ( )}
O g n f n c n R n n f n cg n
# Ω(Big Omega):
0 0
( ( )) { ( ) :g n f n c n, R, n n 0 cg n( ) f n( )}
而我們在說 T(n)=Θ(g(n))其實意義是 T(n)Θ(g(n)),但是那個屬於符號時在是太難打 了…,阿不是是為了方便,所以就記錄成等於符號了(茶)。
嗯其實這個符號的意義對我們來說不是很重要的,只是最好知道一下,以免誤用,且對 於往後真正學習演算法時有個「哦!這我以前看過耶!」的感覺就 OK 了。
反而需要注意的是,因為他漸進意義下所忽略的常數,是有可能非常巨大的,一旦遇到 限制卡很緊時,常數因子的影響力就會表現出來,所以能將常數盡量減小也是該學習的一點。
§1-2 複雜度種類(Kind of Complexity)
最佳複雜度、最差複雜度、平均複雜度。通常我們最關心的就是最差複雜度了,因為對 於競賽來說,如果有筆測資非常之機車+邪惡,那你即使最佳、平均複雜度很低也沒用,還 是會有 TLE 的機會的,所以我們一直希望的就是能夠將最差複雜度降低,當然假如在時空允 許,或者情況很平均分散的話,偶爾也是會犧牲一點最差複雜度來換取平均與最佳複雜度的,
常見的情況就有 qsort, kth element, random construct BST……,這些將會在之後章節作介 紹>.^。
而我們除了估計時間複雜度以外,空間複雜度也是需要注意的。空間複雜度的估計與時 間複雜度差不多,只是是計算這個演算法所需花費的空間大小估計而已。
而又往往為了降低其中一個複雜度另一個就會相對提高。所以啦,在時空(?)間取得其中 平衡就顯得格外的重要了。
§1-3 遞迴複雜度(Recursive Complexity)
因為關於遞迴問題的估計複雜度比較複雜,所以我們往往會把問題變成一顆遞迴樹來思 考。例如式子 T(n)=3(n/4)+cn2就可以化成下列遞迴樹:
log 34
2
2 2 2 2 2 2 2 2 2
4
( ) ( ) ( )
4 4 4
log ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
16 16 16 16 16 16 16 16 16
(1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1) (1)
n
cn c
n n n
c c c
n n n n n n n n n n
c c c c c c c c c
T T T T T T T T T T T T
4
2
2
2 2
log 3
2
3 16 ( 3)
16
( )
: ( ) n
cn cn
n Total O n
因而 T(n)=O(n2)。而有鑑於這類遞迴問題複雜度分析比較困難,所以就簡單說明一下著 名的主定理:
<<主定理(Master Theorem)>>
考慮這樣的遞迴式:T(n)=aT(n/b)+f(n)。
則對於常數 a1, b>1,我們有以下情況:
情況 1: 如果存在常數ε>1,使得 f n( )O n( logba),則 T(n)=Θ(nlogba) 情況 2: 若 f n( ) (nlogba),則 T(n)=Θ(nlogbalgn)
情況 3: 如果對某常數ε>0,有 f n( )O n( logba+)且對常數 c<1 與所有足夠大的 n 有 af(n/b)
c*f(n),則 T(n)=Θ(f(n))
以上定理其實可以看看就好,其大概想法就是利用遞迴形成的樹狀圖來分析。至於詳細證明請自 行參閱《I2A》或相關書籍。
§1-4 小結(Conclusion)
就競賽來說,分析複雜度的意義其實並不是說是最重要的,你常常有可能有高複雜度但 卻可以 AC,也或許有低複雜度卻一直 TLE。但是正確的簡單估計複雜度卻是必要的,不只是 因為筆試有機會考(= =”),也是幫助你決策是否要把題目 co 下去的因素之一,如果明顯 有高複雜度的話那你 co 下去不但拿不到分還浪費時間,又或許你估計錯誤的話你又擔心複雜 度太高而不敢衝一發,所以了學習正確估計複雜度是有實質意義的。
●Section 2 高精度運算(High Degree of Accuracy)
大部分程式語言中的資料型別,其所能儲存的值都有一定的範圍,那若要做的運算超出 這個範圍時該怎麼辦?或所需求精準度很高時該怎麼做?等咻碰嗎?!當然不可能啦,所以 這時候…嘿嘿~就必須自己寫一種資料結構了。
§2-1 大數的資料結構實踐(Data Structure Implementation)
我們分別使用一個陣列來儲存各個位數還有一個變數來紀錄大數。
例如:21474836472147483647 以上用大數儲存就如下
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
7 4 6 3 8 4 7 4 1 2 7 4 6 3 8 4 7 4 1 2
length = 20
※註:因為在 C 的語法中,陣列是由0開始存取,所以實際位數是到length-1。但是以下偽代 碼,陣列都是由1開始到length,這點要注意一下。
§2-2 大數加法(Addition)
一般來說,我們都是利用直式加減乘除法來做運算,所以說以下的運算方法,皆是使用 直式運算方式來思考。
BIGNUM_ADD (bignum a,bignum b) 1 create c as a bignum
2 c.length ← max( a.length , b.length ) 3 for i ← 1 to c.length
4 do c[i] ← a[i] + b[i]
5 for i ← 1 to c.length
6 do c[i+1] ← c[i+1] + c[i] / 10 7 c[i] ← c[i] mod 10
8 if c[c.length] ≠ 0
9 then c.length ← c.length + 1 10 return c
§2-3 大數減法(Subtraction)
基本上有兩種想法,一種是最基本的借位補位直式減法,也就是平常最常用的方法。
再來另一種,就是利用補數的概念,來算減法。
也就是先求被除數的補數,再利用加法將兩數相加,最後在減掉多出位即可。
例如:
2147483647 – 123456789
= 2147483647 + (10000000000 – 0123456789) –1000000000
= 2147483647 + 9876543211 – 1000000000
= 12024026858 –1000000000
= 2024026858
其中 9876543211 為 0123456789的10補數。而至於實際寫法,就請大家自己練習了。
§2-4 大數乘法(Multiplication)
與直式乘法相同,將兩邊的每一位相乘即可。其中要注意的是 a 的第 i 位乘以 b 的第 j 位,會對應到c的第i + j位,所以我們有以下的寫法。
BIGNUM_MULTIPLY (bignum a,bignum b) 1 create c as a bignum
2 c.length ← a.length + b.length 3 for i ← 1 to a.length
4 do for j ← 1 to b.length
5 do c[i + j] ← c[i + j] + a[i] * b[j]
6 for i ← 1 to c.length
7 do c[i+1] ← c[i+1] + c[i] / 10 8 c[i] ← c[i] mod 10
9 if c[c.length] ≠ 0
10 then c.length ← c.length + 1 11 return c
§2-5 大數除法(Division)
既然乘法是連加,那除法可以用連減的方式來解決囉?當然是可以的,只是當商數非常 大的時候,你的連減就會變的非常的慢,而且,我剛剛乘法也不是用連加的不是嗎 XD。
所以我們還是用老方法,直式除法,就像我們平常熟悉的,一位一位去減下來,所以有以下 的代碼。
BIGNUM_DIVIDE (bignum a,bignum b) 1 create c as a bignum
2 b move right for a.length – b.length digits b * 10a length b length. - .
3 for i ← a.length – b.length downto 0 4 do if a > b
5 then c[i] ← c[i] + 1 6 a ← a – b 7 else if a b
8 then b move left for 1 digit b / 10 9 c.length ← a.length – b.length
10 if c[c.length] = 0
11 then c.length ← c.length - 1 12 return c
別看上面寫的短短的,自己寫過就會知道有多不好寫了(笑)。
§2-6 大數開方(Square root)
大數開方有幾種作法,直式開方,十分逼近,二分逼近。
一般來說,寫直式開方會很麻煩,而且也不見得比較快,所以建議還是寫二分逼近法。
而什麼是二分逼近法?就是對數值去做 Binary Search。而十分逼近則是一位一位去逼近,其 實這兩者速度都差不多,但就容易寫的程度來說,二分逼近會比較輕鬆。二分逼近就是利用 所謂 BinarySearch 的技巧,我們將在遞迴部分作介紹。
§2-7 大數優化(Bignumber optimization)
事實上,大數運算還可以運用一些技巧來進行優化,想一想?宣告的陣列本身的空間,
只記一位會不會浪費掉?因此很明顯的,我們可以利用「壓位數」的技巧來使運行速度增快,
而壓位數的意思就是代表陣列裡一格不只存一位的意思,但是壓位數須要注意的是不要不小 心 Overflow 了,特別是乘法部分。而且壓位數的除法會變得複雜一點點,這就留給你們自 己想了。
※建議:本人不建議直接使用 long long 存取(記憶體使用大並且也比較慢),而較建議用 int 存 7,8 位,
只要運算過程記得形態轉換就 OK 了。
§2-8 小結(Conclusion)
以上介紹的就是基本的大數運算,不過上面只討論正數問題,當遇到負數怎麼辦呢,有 小數點又怎麼辦呢?嘿嘿!這時候就必須考驗你的智慧了!!
剛開始的時候大數常常會被直接當成一種考題,但是到後來會變成一種出題者心機的手 段,所以當看到一個題目時,先估計數值範圍是必要的!但是也不要以為,假如輸出答案是 在 int 或 long long 內就不用寫大數,在運算過程中 Overflow 或 Downflow 是常有的事,
所以大數的適用與否,必須經過適當的估計,當用則用,以免造成無法挽回的後果!!
●Section3 排序問題(Time Complexity)
排序演算法,可以說是學習演算法中,獨立出來的一門學問,自古以來都有許多學者專 精於這塊區域。也往往都是學習演算法第一個碰到的問題,以下就對於排序問題作介紹。
所謂的排序問題,顧名思義,就是將一串雜亂無章的數字,將他由小排到大或由大排到 小。在排序問題中,我們有一連串不同的演算法,有快的有慢的,有容易寫的有難寫的,嘿 嘿,這也就證明了這是多麼古老而多人研究的問題了。
我們下列排序方式,都以輸出遞增序列a1 ≤ a2 ≤ a3 ≤ … ≤ an為主。
§3-1 相關名詞定義(Definition for proper nouns)
a. 穩定性質(Stability)
stable 性質的意思是說,對於一個原本在數列中的兩個數 i<j 如果滿足 ai=aj,那麼在排 序以後的序列也會滿足 ai在 aj前方。這性質好像看起來沒什麼用,但如果要排序的陣列存有
其他資料時,這種穩定的性質就會顯露他的重要性了。而基本上有 stable sort 的排序有 Bubble Sort, Insertion Sort, Merger Sort……,但其實 instable sort 只要簡單改一下就可以 做到 stable 了。【想想看如何將 instable sort 改成 stable?】
§3-2 各種常見排序演算法(Sorting Algorithm)
a. 氣泡排序法(Bubble Sort)
氣泡排序法可以說是書上最常見的排序法。每次都走過一次整個陣列,把相鄰兩個中較 大的放右邊,較小的放到左邊,如此重複n次就能完成排序的動作。【想想看為什麼?】
<時間複雜度為O(n2)>
BUBBLE_SORT (A)
1 for i ← 1 to length[A]
2 do for j ← 1 to length[A] – i 3 do if A[j] > A[j+1]
4 then exchange A[j]A[j+1]
b. 選擇排序法(Selection Sort)
選擇排序法同樣是每次走過整個陣列,不同的是它是在走的過程中紀錄最小的,最後在 將他放到後面去,同樣是要重複 n 次。
<時間複雜度為O(n2)>
SELECTION_SORT(A)
1 for i ← 1 to length[A]
2 do t ← i
3 for j ← i + 1 to length[A]
4 do if A[j] < A[t]
5 then t ← j 6 exchange A[t] ↔ A[i]
c. 插入排序法(Insertion Sort)
插入排序法是假設前i-1個已經排序完成,在插入第 i 個數的時候,就只需要一個一個比 較,找到適合的地方放進去,最後輸入完,也就剛好排序完了。
<時間複雜度為O(n2)>
INSERTION-SORT(A)
1 for i ← 2 to length[A]
2 do j ← i – 1
3 while j > 0 and A[j + 1] < A[j]
4 do exchange A[j + 1] ↔ A[j]
5 j ← j – 1
以上三個為一般常見,為較慢的O(n2)排序法,接下來就要介紹比較快速,也是較常用的 排序演算法。
d. 合併排序法(Merge sort)
合併排序法,用到我們之後所要講的 D&C 來解決排序問題(詳細可見後一節)。這個方 法首先是先將資料一分為二,然後分別排序這兩組資料。最後在合併起來。而此方法最重要 的就是,如何將兩堆已排序好的部份在O(n)時間內合併。
【先想想看,如何將兩個已排序數列在O(n)時間內合併?】
<時間複雜度為O(nlgn)。>
MERGE-SORT(A, p, r) 1 if p < r
2 then q ←
(pq)/2
3 MERGE-SORT(A, p, q) 4 MERGE-SORT(A, q + 1, r) 5 MERGE(A, p, q, r)
MERGE (A, p, q, r) 1 n1 ← p – q + 1 2 n2 ← r – q
3 create arrays L[1..n1+1] and R[1..n2+1]
4 for i ← 1 to n1
5 do L[i] ← A[p+i-1]
6 for j ← 1 to n2
7 do R[j] ← A[q+j]
8 L[n1+1] ← ∞ 9 R[n2+1] ← ∞ 10 i ← 1
11 j ← 1
12 for k ← p to r 13 do if L[i] ≤ R[j]
14 then A[k] ← L[i]
15 i ← i + 1 16 else A[k] ← R[j]
17 j ← j + 1 e. 快速排序法(Quicksort)
快速排序法同樣也是利用 D&C 的技術來解決問題。
他是先任取一個數當作標記,把比他小的數放他前面,比較大的數排他後面。再將這兩 堆去做排序,就這樣一直遞迴下去,最後就完成排序動作了。
<時間複雜度在一般情況為O(nlgn),在最糟狀況可達O(n2)>
【想想看,最糟情況是什麼時候?】
【想想看,有沒有好方法選擇標記數,使得平均速度快些?(較不會遇到最糟情況)】
QUICKSORT(A, p, r) 1 if p < r
2 then q ← PARTITION(A, p, r) 3 QUICKSORT (A, p, q) 4 QUICKSORT (A, q + 1, r) PARTITION (A, p, r)
1 x ← A[r]
2 i ← p – 1
3 for j ← p to r – 1 4 do if A[j] ≤ x 5 then i ← i + 1
6 exchange A[i] ↔ A[j]
7 exchange A[i + 1] ↔ A[r]
8 return i + 1
f. 其他排序法(Other Sorting Algorithm)
以上的排序法是經由比較和交換來完成的,我們把它叫做比較排序法(Comparison sort),這種排序法已經被證明其時間複雜度的下限為Ω(nlgn)。
但是對於某些特殊的資料,我們有一些方法可以減少到O(nlgn)以下的複雜度,以下介紹常見 幾種。
(f.1)計數排序法(Counting Sort)
計算每個數字出現過幾次,然在由小到大的填入原本的陣列。
<時間複雜度為O(n+m)> 其中m為輸入數值範圍。
COUNTING-SORT(A, k)
1 initialize C[1…m] = 0 m is the upper bound of C 2 for j ← 1 to length[A]
3 do C[A[j]] ← C[A[j]]+1 4 t ← 1
5 for i ← 0 to m 6 do while C[i] > 0 7 do A[t] ← i 8 C[i] ← C[i] – 1 9 t ← t + 1
這是一個很快的算法。可是缺點非常耗記憶體,且數值範圍大的時候沒辦法使用,假如 有負數也必須要做修正,當然這也只適用於整數排序而已。也因此常常是用在出現於將小數 據範圍的正整數做排序(m=O(n))。
(f.2)基數排序法(Radix Sort)
先照低位數排序,再照次低位,再第三位…這樣下去,這跟一般想法不太一樣,他排序 順序是低到高,平常想到都是高到低,所以對每位數排序時,就必須要有某些性質。【想一想?
哪些性質?】
<時間複雜度為O((n+B)logBm)>其中B為進位制,m為數值範圍
基本上這些特殊的線性時間排序法(Linear Time Sorting)一般來說是不太會用到的,往往 只有當遇到時間卡很緊等等的時候才會真正拿來寫。不過這些想法基本上必須要了解一下,
或許以後在處理問題時,也可以利用一些類似的想法來處理,例如說 Edge List 的製作(詳 見往後章節)就可以用到 Counting Sort 的想法,而 Suffix Array 的倍增算法中也會頻繁用 到這兩種排序法。
§3-3 內建的排序函數(Sorting Library Function)
(1) qsort
void qsort( void *buf, size_t num, size_t size, int (*compare)(const void *, const void *) );
C 內建的 Quick Sort,需要注意的是在 compare 函數裡要將 void 轉成所需的資料型態。
(2) sort
void sort( iterator start, iterator end );
void sort( iterator start, iterator end, StrictWeakOrdering cmp );
STL 內建,使用的是 intro sort,最差與平均情況都是 O(n lg n),intro sort 其實就是多 種排序演算法的混合,根據不同算法在不同大小時候的表現選擇算法,使得它的速度比 qsort() 快。
§3-3 排序的應用(Sorting Application)
排序是基本而重要的,在各種問題中,常常預處理都跟排序有關係,雖然實際比賽的時 後大部分都不用自己 co 排序,只要用內建函式就好了,不過,是希望大家除了使用內建函式,
更重要的是要知道其真正想法,因為有些題目會必須利用到排序的想法,例如說下面這兩題:
問題一《逆序數對》(TIOJ1080,逆序數對)
對一個數列S來說,若S的第i項si與第j項sj符合si > sj,並且i < j的話,那麼我們說(i, j) 是一個逆序數對。請問給定數列S,請問總共有多少個逆序數對呢?
問題二《第 k 大的數》(TIOJ1364,蛋糕內的信物)
對一個數列S來說,要如何在O(n)的時間內找出第k大的數呢?
像這種時候就沒辦法使用依賴內建函式了,所以說,練習自己寫O(nlgn)的算法就變的很重要 了。
相關題目:TIOJ 1205, 1208, 1287
●Section 4 幾個常用的數學演算法(General Math Algorithm)
§4-1 最大公因數(Greatest Common Divisor)
最大公因數,是在數論相關問題中,常常會用到的一環,當遇到關於數學的程式題時,
常常會用到最大公因數,那麼,怎麼樣快速而有效的求出最大公因數呢?
有一個方法是,對於每個小於a,b的數,去試試看能否整除兩者,其中最大的便是最大 公因數了,但是,這樣時間是O(min(a,b)),還是不夠快。
那另一種方法是說,將兩者因數分解,在去比對公因數,不過這樣子不但 code 起來麻 煩,也不會快多少。
事實上,這個問題可以回溯到古希臘時期……,Euclid 發明了一種方法,叫做 Euclid 輾 轉相除法,他利用公因數的性質,快速的求出了最大公因數d ( , ) ( , -a b b b ma) 其中m, 所以其實又可以寫成d ( , ) ( , mod )a b b b a ,你看出遞迴關係了嗎?試試看吧。【可以試著 去思考什麼樣的數字會形成最差狀況?而其複雜度又為多少?】
GCD 應用:
利用 Euclid 輾轉相除法,我們在求解例如 ax+by=d 的時候,可以很容易的求出滿足條 件的 a 與 b。而這個的應用對於解決數論相關問題有重要的影響,當要求模的逆時,就可以 使用這種做法了。【想想看,怎麼求?事實上只要在函數中紀錄一些東西,就可以輕鬆算出來 了。】
§4-2 質數(Prime Number)
相同的,常常在遇到數學相關問題時,都會用到質數,那如何快速求出質數,就變成非 常重要了。因為質數分布是非常離散而無規律的,似乎沒有什麼快速的捷徑法,那麼要如何 加快求質數的效率呢?利用質數的定義,我們可以知道,要驗證一個數是否為質數,可以對
於2到 p
所有的整數,如果能除盡他,就代表他不是質數【想想為什麼?】,看起來這個 方法還不錯,不過,假如你是要驗證一個數是否為質數,這樣還 OK,但是一但你要建立質 數表,這種方法就行不通了。
因此,我們又回到古希臘時期,發現,原來兩千多年前就有人想出如何建立一個質數表 了,這叫做 Eratosthenes 篩法,事實上他只是把我們剛剛的想法反過來而已。簡單的說,他 是把每個質數i當成篩子,把i的倍數全部過濾掉,當你對於小於 n的質數都篩完後,同 時代表你找出所有小於等於n的質數了。
ERATOSTHENES(n)
1 make array p[1...n] with value = 1 initialize 2 m[1] ← 0
3 for i ← 2 to sqrt(n)
4 do if p[i] = 1 i is a prime number 5 then for j ← i × i to n step i
6 do p[j] ← 0
【思考一下為什麼第二層迴圈起點為 i*i?】
因數分解
要怎麼質因數分解呢,當然就是對於小於他的質數,去除除看,過程中紀錄一些資訊就 完成了。質數還有許多應用,特別是之後在 Hash 表的時候可以拿來用,等到往後章節再做 介紹。
●Section 5 遞迴與分而治之(Recursion & Divide and Conquer)
「以此要領,重複實施,即得答案。」-pangfeng
分而治之,顧名思義,就是把問題分成幾個小問題,然後再一個一個去解決的一種方法。
為什麼要用遞迴呢?通常來說,對於問題給定輸入值很小的時候,我們會很容易的解決他,
而當我們能利用這些容易解的小小子問題,來將較大母問題解決時,我們便可以選擇使用遞 迴演算法。通常寫出遞迴的 code 都不會太長,但是,遞迴常常會出現重複子問題、遞迴過 深等種種的問題,最嚴重的,便是他的複雜度,往往都是指數級成長,這時候,如何改善遞 迴式,或適當的優化,就變得頗重要了。
以下就舉幾個例子,來讓你們了解遞迴與分而治之的用途。
§5-1 簡單的例子(Easy Example)
a. 階乘(Factorial)
明顯的我們求N!可以化作(N 1)! ( N 1) N!而當N = 0時0! = 1,故我們可以寫出以 下遞迴式。 ( ) 1 , 0
( 1) , 1
f n n
n f n n
,等寫出了式子,接下來要 code 就簡單多了。
如這個例子就可以寫成:
FACTORIAL(n) 1 if n = 0
2 then return 1
3 return n × FACTORIAL(n - 1)
b. 費氏數列(Fibonacci Sequence)
相信大家都知道費氏數列表示可以表示成F n( 2)F n( 1) F n( ),特別的有
(0) (1) 1
F F 。
寫出遞迴式後,寫出 code 就是輕而易舉的事了。
FIBONACCI_NUMBER(n) 1 if n ≤ 1
2 then return 1
3 return FIBONACCI_NUMBER(n - 1) + FIBONACCI_NUMBER(n - 2)
§5-2 遞迴的時間複雜度(Time Complexity of Recursion)
參考《Section1 -3 主定理》的介紹。
§5-3 問題討論(Problem Discussion)
相信大家對遞迴有一定的了解了,但是,以上兩題都比較偏向數學,接下來的想法與實 踐,就是比較困難一點點了,所以請大家踴躍提出想法與熱烈討論。
問題一《河內塔》(TIOJ 1355, 河內之塔-蘿莉塔)
現在有三根柱子,一開始在第一根柱子上有n個圓盤,由大到小疊好,請印出所有搬動圓 盤步驟,把所有圓盤從1號柱搬到3號柱子,且移動過程中大圓盤不得疊在小圓盤上。
問題二《冪運算》(Uva 374, Big Mod)
給定B, P, M,試以O(lg P)的複雜度算出BP mod M。
問題三《Binary Tree Reconstruction》
給定一棵二元數的 pre-order 以及 in-order,求 post-order。
前序(pre-order): 根節點左子樹右子樹(ABCDE)
中序(in-order): 左子樹根節點右子樹(BADCE)
後序(post-order): 左子樹右子樹根節點(BDECA)
問題四《Joseph Problem》
n個人編號1至n圍成一圈,現在由第一個人開始數,每數k個人就把第k個人殺掉,並 由下一個人繼續開始數。試問最後會活下來的是編號多少的人?
問題五《八后問題》(Uva 750, 8 Queen Chess Problems)
在西洋棋的棋盤中你可以放置8個皇后而且彼此都不衝突(就是都不能吃到對方)。給你某 一個皇后的位置,請你寫一個程式來輸出所有這樣可能的安排。
問題六《Savage Garden》(Uva 10230, Savage Garden)
在一個長為2n的正方形盤面上,挖去了一個空格。試用「L 形」拼圖(總共 3 格所構成的)
把整個盤面拼滿。
問題七《三色多邊形》
有一個N邊形,且每個頂點上都有紅綠藍三種顏色其中一種,且兩兩相鄰頂點顏色互不相 同,三種顏色一定都會出現!現在要你畫出許多對角線將此多邊形分割,分割成N - 2個三 頂點恰為三種顏色的三角形,而任兩條對角線只可能相交在頂點上,或者不相交。
問題八《丟失的數》(TIOJ 1358, 丟失的數)
現在有n-1個相異數,範圍是0 ~ n-1。請問0 ~ n-1中沒有出現在這n-1個數的是哪一個數 字?每次可詢問第i個數二進位表示法的右邊數來第j位(Bi,j),請使用盡量少次的詢問。
A
B C
D E
問題九《最近點距對》(TIOJ 1500, Clean up on aisle 3)
給你平面上 n 個點(n ≦ 106),問你說所有點對中,距離最近的兩個為何?
§5-4 二分搜尋(Binary Search)
在一個已序的數列中,找出一個給定的數的位置。
基本的想法就是利用數列「已經排序好」的這個性質,每次都將想要找的數和正中央的 那個數比較,並依照比較結果可以知道要找的數是在左半還是在右半,這樣一直重複下去,
可以在 O(lgn)的時間內找到要找的數。
BINARY_SEARCH(A, x, y, key) 1 while x + 1 < y
2 do m ← ( x + y ) / 2 3 if key < A[m]
4 then y ← m 5 else x ← m 6 if A[x] = key 7 then return x
8 else return NOT_FOUND
【想一想】:當有相同的元素時,題目要求要找出 (1)最前面的一個 (2)最後面的一個,要怎 麼樣處理呢?
但是如果你要的東西沒排序好怎麼辦呢?很明顯的,這時候排序的算法就派上用場了。
因此二分搜往往都會已排序來當預處理的程序。
二分搜尋看起來頗簡單呀?我幹嘛要特別拿出來講呢?那是因為二分搜尋實在是一種很 美妙的東西,能夠把複雜度 O(d)降到 O(lgd)。適當的使用你就會發現人生更美好。
二分搜出現的情形有很多種類型,以下就來看看簡單的幾題:
問題一《Dark Rank》(TIOJ 1360, Dark Rank)
給你一個 m×n 的表格,我們想找出每橫排中最大的數在哪,而我們已經知道這個表格有 個性質,就是每橫排最大值會在前一排最大值的右邊。每次可以詢問任一格的值,請用盡 量少的次數來找出每橫排的最大值位置。
問題二《尋找神秘的胖子》(TIOJ 1525, 尋找神秘的胖子)
給某個圓中的一個點,但此圓的資訊不告訴你,不過你每次可以尋問在平面上某個點是否 在圓內,現在希望你再利用盡量少的次數,詢問出此圓的半徑。
基本上二分搜的應用很廣,往往會結合往後所學東西(如 Greedy, DP, Simulation……),往 往都是假設答案值後,再去驗證這個答案是否有辦法滿足條件,最後再對這個答案值做 Binary Search。但實際應用必須等學到往後東西才有辦法說明,因此在此就先不提及。
●Section 6 離散化(Discretization)
離散化在解決很多問題時是很常用的技巧,當你要處理的問題資料量很小,但是資料分 步很分散的時候,就可以使用到離散化的操作。其基本思想就是在眾多可能的情況中「只考 慮我需要用的值」。
下面舉個簡單的例子來說明:如果現在在一條長長的走廊上有10個垃圾,但是從第一個 到最後一個垃圾的距離有一公里,現在我想用掃把將垃圾掃掉,是不是要從第一個開始慢慢
掃到最後一個呢?(意思是整條走廊都被掃到),很明顯的,廢話當然不用呀!我所要做的就 是走到第一個垃圾的地方掃起來,再走到第二個地方掃起來…,最後走到最後一個垃圾,並 掃起就好了。
由上面這個例子來看,我所需要考慮的就只是「我所要掃的垃圾的位置」,而不是整條走 廊。這就是一個很好的例子,相信大家對離散化也有些概念了。
而離散化實踐很簡單,通常我們會將整個資料先利用O(nlgn)時間排序,並給排序後的值 標上記號,以後處理資料則照標號就好了。但要注意的是,因為是使用預處理(pre-process)
的技術,因此對於動態存取的問題則沒辦法使用離散化來處理。
而其要處理的問題往往都與計算幾何有關(當然也並不全然),總之下面幾個問題讓大家 思考一下:
問題一《Skyscrapers》
一條直線上並排著 n 棟建築物,每棟建築物有他的高度 hi。因為最近淹大水,所以如果建 築物高度不夠高則會被水淹沒。現在給你 d 天淹水的高度,問你說每天會有多少連續段建 築物不會被水淹沒?(n, d <= 106)
問題二《Shaping Regions》(Usaco3-1, Shaping Regions)
現在有一張白紙,並且要將不同大小的色紙貼在白紙上(n<=1000),假如兩塊矩形面積 有重疊,那後放的一定在上面,現在給你每個色紙大小與放的順序,問你說最後每個顏色 覆蓋多少面積?
Chapter 2 基礎資料結構(Basic Data Structure)
●Section 0 什麼是資料結構?(What is Data Structure?)
資料結構,也就是一種特別的資料儲存方式,就像 int, long long, float, double……等 等,只不過算是自己自訂型態的一種結構,而這種結構,能夠幫助我們加快執行的速度。當 然,有些資料結構的意義不在於他的實做,甚至只是他的想法。所以,其實資料結構並不一 定是自己定義的一種型態,也可能是使用既有的資料型態幫助實踐,以下我們就來介紹幾種 常見的資料型態。
●Section 1 陣列(Array)
陣列,是大部分程式語言都內建的一種資料結構,也是最基本的,所以其功用在此不再 多言。重要的是,array 是各種資料結構應用的基礎,如果不會寫 array……,嘿嘿,那就等 著咻碰吧 XDDD(茶)。
●Section 2 串列(Linked-list)
Linked list,主要想法是指一群散佈在各個地方的 node,藉由某些方式連結,而通常是 一串藉由「指標」連結在一起的一種自訂資料結構。一般來說有分雙向與單向,雙向串列 (doubly linked list)如下圖:
tail[L]
key next prev
/
2 3 8
/ 5
head[L]
Linked list 是個有彈性的動態結構,而其主要與 array 的操作不同是:Insert 和 Delete 操作,我們只需要 O(1)時間,但是這並不包含 Search 的時間,要 Search 第 k 個元素,就必 須用掉 O(n)的時間。
§2-1 串列的實踐(Implementation)
串列的實踐,最簡單有兩種方法:
(1) 指標直接實踐:明顯的就是定義個 struct,用指標來連結下個 node,每增加或刪除一個 節點就 new 或 delete 一個空間,這樣也是最省空間的方式。
(2) 使用陣列模擬指標:預先開好個結構陣列,其中有兩個變數,分別是是存取 next 與 pre 的索引值(index),例如下圖:
L 4
8 3 5 2
7
3 4 3
1 7 /
/
8 7 6 5 4 3 2 1
prev key next
※建議:個人是比較推薦第二種,因為配置與刪除記憶體空間速度較慢,且較容易發生不預 期的錯誤。
§2-2 哨兵(Sentinels)
當用指標來實踐 linked list 時,頭或尾插入或刪除元素,會因為 NULL 的狀況需要考慮,
所以造成許多麻煩,這時我們可以使用一個叫做哨兵的東西,也就是多創個新的節點,來代 表 NULL,如下圖:
2 3 8
5 nil[L]
Linked list 應用是非常廣的,建構一棵 tree,一個 graph,都是常用的手法,或者是在 某些模擬(simulation)題中,也是很實用的。基本上你要將 linked list 看成跟 array 一樣 的基本結構,這兩個資料結構幾乎是所有資料結構的基礎,常常實踐都必須靠這兩者。
事實上,Linked list 一開始寫的時候,並不會那麼容易上手,因為指標的想法並不是說 那麼容易,不過當你多練習幾次之後,就可以漸漸上手。
●Section 3 堆疊(Stack)
堆疊,顧名思義,就是一種將各個元素堆在一起的一種結構。想像你把一堆書疊在一起,
假如你想拿一本書,你就必須從上面開始拿;假如你要放一本書,你不會插在中間,一定是 往上疊。是的!堆疊就是像一疊書的資料結構,並且支援兩種操作,丟入(PUSH)、丟出
(POP),如下圖:
Stack 結構的實作其實只需要用到 array 就夠了,因為只有一個出口,你只要記錄 top 的位置就夠了。
對於這種先進後出,後進先出的機制,我們稱作FILO(First-In-Last-Out)。你會想說這 麼簡單的結構,有啥鳥用?事實上對於解決許多問題,都會用到 stack 的想法,例如說,平 時在 recursion 的時候,系統就會建立一個 function stack,以存取函式資料內容,所以了,
當你遞迴過深,記憶體將無法負荷!
以下幾道問題,就是利用 stack 的想法來解題。
問題一《火車調動》(Uva 514)
堆疊市的火車站長得如右圖一般。火車車廂由 A 處依1, 2, 3, …, n的順序進站。如今給定一個1到n的重排,試問是否能在 station 處經過一些調度,使火車依照這樣的重排順序出站。
問題二《合法括弧》
有若干種括弧,如(), [], {}, <>。給定一串括弧,試問其是否為合法?(能不交錯地配對)
例如({<()[]()>[]})是合法的,([]}和<>>和[[]都是不合法的。
註:長度為 n 由’(‘和’)’構成的合法括弧字串──卡特蘭數 Catalan Number。
top
top
PUSH
POP
問題三《運算式》(Uva ACM 727,Equation)
將一個中置運算式 in-order改成後置運算式 post-order。(運算元最多只有一位數)
中置運算式:其實就像我們一般寫的式子,可以有括弧,如(3+2)5、((1+2)(3+4)+5)(6+7) 後置運算式:運算子放在運算元後,沒有括弧,如32+5、12+34+5+67+。
問題四《排隊視線問題》(TIOJ 1176,Cows)
給定一串數列有n個數,試在O(n)時間求出每個數在它之後第一個比它小的數是多少?
例如對數列2, 0, 7, 8, 3, 6, 4, 5, 4, 1,求出的結果是0, -, 3, 3, 1, 4, 1, 4, 1, -。
問題五《最大矩形 part1》(TIOJ 1370,超大鏡框設置)
現在有一堆不同高度的建築物排列且連在一起(如右圖),現在你想 要用相機把這群建築物拍下來,而且因為天空太髒了,所以不想拍 到天空,假設相片的長寬可以任意伸縮,試問最大可以拍出多大面 積的照片?
挑戰一《最大矩形 part2》(USACO Section 6.1, A Rectangular Barn)
在一個零一矩陣中(長度不大於1000),找一塊最大的長方形,裡面全部都是 0。
試問這樣的長方形最大的面積為多少?
挑戰二《最大矩形 part3》(TIOJ1283,超大畫框設置)
現在有個這樣的框架:只任何一個水平線截出的框架區段是連續 的,並且由上往下該區段只會往右移動的框架(如左圖)。
現在你想找到一個最大面積的矩形位置放置你最喜愛的一幅畫。
挑戰三《Cartesian Tree 笛卡耳樹》(TIOJ 1549 胖胖國小的疊羅漢)
給你一個陣列 A[1…n],你希望建立一棵樹滿足以下性質:
這棵樹的 root 是 A[1...n]中最小的值。
而 root 的左子樹是 A[1...root-1]所形成的 Cartesian Tree。
而 root 的右子樹是 A[root+1...n]所形成的 Cartesian Tree。
●Section 4 佇列(Queue)
§4-1 佇列(Queue)
佇列(Queue),就是一種像排隊的機制,先來的人先買票入場,後來的人後買票入場,
也就是 FIFO(First-In-First-Out)。因為 queue 列有兩端,所以我們分別用 front 跟 rear 來 記錄 queue 前端與後端。
基本操作有兩個:
push(enqueue):在 queue 前端插入一個新元素。
pop(dequeue):將 queue 後端元素 pop 掉。
就如右圖所示,push 就是將 front++,pop 則是 rear++。
實踐則是可以使用陣列或串列,並使用兩個指標指向 front 跟 rear,差別在於每次 pop 時陣 列無法釋放記憶體,而串列則可以。但是這樣不是很浪費記憶體嗎?因此我們可以用環狀的 結構叫環狀佇列(Circular Queue),來作實踐,顯而易見的,如果要實踐環狀佇列,只需要 使用 mod 操作便可達成。然而因為空間大小是固定,所以假如一開始大小還是不夠大,即 使是環狀最後還是會爆炸的。
queue 的應用方面,大部分是在 graph 上,之後教到的 BFS,就會使用到 queue 這樣 的資料結構。另外像電腦鍵盤跟印表機也都是使用 queue 來記錄作業優先順序的。
§4-2 雙向佇列(Double-ended-queue,Deque)
主要結構與 queue 相同,唯一不同者就是 deque 的兩邊都可以 push 跟 pop。對於往 後遇到的一些能利用題目單調性來減少時間的算法,有很重要的幫助。
例如說現在有一段區間,假設你想至左而右詢問不同的區間的最大值為何時,如何使用 O(n)將所有問題回答呢?(假設第 i 次詢問的區間是[Li, Ri],那其中至左而右的意思就是對 所有 i<j,滿足 Li<=Lj且 Ri<=Rj,如下圖)
在 deque 中要維護的性質有兩個:
A. 對於原區間中的兩個數 ai、aj且 i<j,如果兩數都出現在 deque 中,那 ai也會在 aj左方,
簡單的說就是原區中的順序到了 deque 中也不會改變。
B. 在 deque 中的値必定是「非嚴格遞減」的。也就是(dequerear ≧ dequerear+1 ≧ ··· ≧ dequefront-1≧ dequefront)。
有了這樣的約定,我們可以知道 deque 中最左端的値(dequerear)必是此區間最大值。
而為了維護整個 deque 的性質,我們可以做以下兩個操作:
(1)詢問的區間右界右移:
像 stack 在維護由下而上遞減性質一樣,假設新加入的値是 ai。 如果 dequefront>ai,則直接 push_front(ai );
如果 dequefront ≦ a,則先 pop_front(ai i ),直到 dequefront>ai,再 push_front(ai )。
可以這樣做是因為對於所有右界在新元素左方的詢問已經問完了,所以 pop 掉數字小的
元素情況一定不會比較差。
(2)詢問的區間左界右移:
這就比較簡單了,如果最左方的元素剛好對應到要刪掉的元素,那就可以 pop_back 了。
§4-3 優先級佇列(Priority Queue)
雖然名為 queue,但是這個只是借用 queue 的名字,事實上跟 queue 相差甚遠,它並 不是 FIFO,而是每次 pop 元素,一定會是佇列中「優先級最小」的元素,至於實踐上,通 常是用 heap 來實踐,這將在之後課程會介紹。
stack &queue 相關題目:
ACM 127, 271, 239, 442, 511, 514, 727
TIOJ 1012, 1063, 1237, 1283, 1320, 1566, 1574
●Section 5 樹狀結構(Tree)
§4-1 何謂樹?(What is Tree?)
樹是一種很特殊的圖(Graph),基本上就是一棵上下顛倒的樹,相信大家應該看過樹狀 圖之類的東西吧?而在資訊中常用的樹大概就是長那樣,如下圖:
§4-2 定義(Definition)
樹原本定義為一個連通但其中不含任何的圈的圖,但是卻有以下性質,且這些性質與G 是一棵樹其實是等價的:
(1) G是一個樹。
(2) G為連通且沒有圈。
(3) G為連通且| E | = | V | - 1。
(4) G中任兩點恰好有唯一一條路徑連接。
(5) G為連通,但是去掉任何一條邊後都會變成不連通。
(6) G中沒有圈,但是加上任何一條邊後都會變成有圈。
這裡我們說的樹一般指的是「有根樹」(Rooted Tree)。有根樹就是將其中一點指定為根節點
(Root),而其他點的深度定義就是他和根節點的距離。
對於以上定義不太清楚沒關係,圖的詳細介紹將會留至《Chapter5-圖論》再做介紹。
§4-3 相關名詞定義(Definition for proper nouns)
⊙ 父節點(parent node)、子節點(child node):如果a和b有邊相連,且b的深度比a的 深度多一,則我們稱a是b的母節點,b是a的子節點。一個節點可能有很多子節點,但只會 有一個母節點。
⊙ 兄弟節點(sibling):有相同母節點的兩個節點。
⊙ 度數(degree):一個節點的子節點數目。
⊙ 葉子(leaf):沒有子節點的節點。
⊙ 高度(height):我們稱一棵樹中節點的最大深度為該樹的高度。
⊙ k元樹(k-ary tree):如果一個樹每個節點的度數都不大於k,則稱此樹為一個k元樹。
⊙ 子樹(subtree):以某一個節點為根節點,並取所有他以下的節點和邊形成的樹。
⊙ 森林(forest):就是很多很多棵的樹(其實就是沒有圈的圖)
§4-3 樹的表示法(Representation of Tree)
要如何來儲存一棵樹呢?我們先考慮最簡單的情況:二元樹,這時可以用兩個指標分別 儲存左子節點和右子節點的位置。
Struct Node{
DataType data;
Struct Node* ParentNode;
Struct Node* LeftChild;
Struct Node* RightChild;
};
或是假如你知道了這棵樹是完全二元樹(complete binary tree),或是它的深度不深,沒 有很歪斜,那麼可以較簡單的用陣列來儲存一棵樹。令A[1]為根節點,對於一個節點A[n],
它的子節點是A[2n]和A[2n+1],母節點是A[n/2]。
用同樣的方法,假如我們知道了一個樹是k元樹,也可以用一個指標陣列來指向它的所 有子節點。
Struct Node{
DataType data;
Struct Node* ParentNode;
Struct Node* ChildNode[k];
};
假如這棵樹沒有任何的限制的話,有一個叫做Left-child-right-sibling的方法可以來記錄 它。就是對於每個節點,記錄它最左邊的子節點,和他右邊相鄰的兄弟節點。雖然轉換會較 為麻煩,但這個是紀錄任意樹的比較合理的方法,尤其是它需要的記憶體用量會比上面那種 k元樹紀錄法還少的多。
Struct Node{
DataType data;
Struct Node* ParentNode;
Struct Node* LeftChild;
Struct Node* RightSibling;
};
§4-4 二元樹的走訪(Traversal of Tree)
對於一個二元樹,我們有三種最常用的方法可以走過這棵樹所有的節點。
1. 前序表示法(pre-order)根節點 -> 左子樹 -> 右子樹 2. 中序表示法(in-order)左子樹 -> 根節點 -> 右子樹 3. 後序表示法(post-order)左子樹 -> 右子樹 -> 根節點
這三種方法都可以用遞迴來實現,而有趣的是,只要知道任意兩種表示法,就可以確定這 棵樹進而得到第三種表示法,這在遞回那節時已提過,因此就不多做說明。
§4-5 二元搜尋樹(Binary Search Tree - BST)
二元搜尋樹是一種二元樹,並且滿足對於任何一個節點,它的左子樹的節點的值都比它 小,它的右子樹的節點的值都比它大。而對二元搜尋樹,我們可以進行幾種操作。
(1) 尋找元素(Find):
和二元搜尋的概念一模一樣,從root開始,現在的數值比要找的數值大就往左走,如果比 較小就往右走,相等的話就找到了。
(2) 插入元素(Insert):
和尋找元素的時候一樣,從root開始找,直到走到一個葉子為止,然後把它加在該葉子的 子節點。
(3) 刪除元素(Delete):
刪除元素時比較麻煩,如果要刪除的節點是x,則有三種情況:
* 如果x是leaf,就直接刪除就好。
* 如果x只有一個子節點,則把該子節點連到x的母節點,並且刪除x。
* 如果x有兩個子節點,我們選取左子樹最右邊的一個元素y替代x,並且刪除y。
如果二元搜尋樹的高度是h,則上述三個操作的時間複雜度都是O(h)。
我們知道,如果一個二元樹是完全的,那麼它的高度為O(lgn),所以上述演算法的複雜度 就是很快的O(lgn)。但是這樣建立出來的二元搜尋樹往往是不完全,甚至於很不平衡的,在 最糟糕的情況下,全部元素排成一個直線時,h就變成O(n)了。
為了預防這種情形,我們可以在建立時將增加點的順序用成隨機的,可以證明隨機後的 BST的深度會是O(lgn)的,但是假如你的輸入不是一次給完的話(也就是要求要是在線算
法),那麼這招便不可行了。
而其實還有各種的平衡樹(balanced tree),例如AVL-tree、Red-Black tree、Treap、Splay 等等,但是因為這些的方法都很複雜,所以在這裡就不介紹了。
其實樹還有很多的應用,像後面會講到的Heap就也是一種二元樹,Disjoint Set是一種森 林等等。所以樹是相當相當基本且重要的一種資料結構呢!
問題ㄧ《約瑟問題》(TIOJ1383,約瑟問題)
有 n 個人圍成一圈,從第一個人開始數,每個人的椅子都被裝上"強制脫出裝置",現在給 你 n 個數,代表每一次要數幾個人之後彈出,請問你人被彈出的順序?
相關題目:TIOJ 1106, 1108, 1213, 1214