• 沒有找到結果。

簡介、STL 資料結構及algorithm 初階

N/A
N/A
Protected

Academic year: 2023

Share "簡介、STL 資料結構及algorithm 初階"

Copied!
12
0
0

加載中.... (立即查看全文)

全文

(1)

簡介、STL 資料結構及 algorithm 初階

Ting.H

2017 年 9 月 14 日

1 未來的任務

1.1 主要任務

時間 任務名稱 地點 簡介 9/12 校內資訊能競初賽 建中 前天(?)

9/29 校內資訊能競複賽 建中 取十幾人成為校隊

10/27 北市賽 台北 一二等獎(10人)進入全國賽

12月 全國賽 台灣 一二等獎(10人)進入TOI一階

2月 校隊補選 建中 遞補已進TOI一階的名額

3月 TOI入營考 師大分部 前20名進入TOI一階

3,4月 TOI一階 師大分部 14天兩次考試,選出12人進入TOI二階 4月 TOI二階 師大分部 4人成為國手

5月 APIO 師大分部 由TOI二階選手參加

9月 IOI Japan Tokyo 為國爭光

1.2 次要任務

時間 任務名稱 地點 簡介

11月 北市軟體競賽 大安高工 初賽是筆試、複賽是上機

11,12月 NPSC 台大 同校三人組隊報名。有初賽和決賽,每校最多三隊

進入決賽。去年獎品好像不錯。

I

(2)

1.3 OJ

簡稱 網址 介紹

TIOJ tioj.infor.org 全名是TIOJ INFOR ONLINE JUDGE,是建中的OJ據說已有幾十

年的歷史,有很多好題,但當然也有不少爛題。

UVa uva.onlinejudge.org 可稱為史上第一個OJ,題目多到寫不完,裡面還有不錯的題單。

但如果有英文障礙,應該就會很吃力。

ZJ zerojudge.tw 有一些UVa題和入營考題以及不少水題。

CF codeforces.com 一段時間就有比賽,可以爬世界排名,但時間通常是半夜。

POI szkopul.edu.pl 波蘭資奧的網站,裡面有很多不錯的題目,好像不能用c++11。

1.4 網路資源

名稱 網址 介紹

演算法筆記 http://www.csie.ntnu.edu.tw/u91029/

有很多競賽會用到的演算法的介紹,但是 裡面有些錯,所以如果你覺得有問題,那 就應該是他寫錯了

c++ reference http://www.cplusplus.com/reference/ 有C++內建程式庫的介紹,有點難懂,但 十分詳細。

資訊之芽 www.csie.ntu.edu.tw/sprout/algo2016/

這是一個課程,網站上有講義、課堂PPT

、回家作業等,也有一些課堂影片,可以 去看看。

2 複雜度介紹

2.1 Big O 函數

在介紹複雜度之前,我們先引進一個函數,Big O。Big O的定義如下:

f(N) =O(g(N)), if ∃N0, c >0,∀N > N0,|f(N)| ≤c|g(N)|

這個的意思是說存在一個常數c,在N 足夠大時,讓f(x)不大於g(x)的c倍。

對於一個多項式如f(x) = x2+ 2x,可以是O(x2),因為當x >2時,x2+ 2x不大於x2的 2倍。當然也可以寫成O(x3)、O(2x2)等等但一般而言我們都會寫O(x2)。

仔細想一下就會發現 Big O是複雜度的上界,而還有另外兩個複雜度的符號 Ω(N) 和

Θ(N),定義如下:

II

(3)

f(N) = Ω(g(N)), if ∃N0, c >0,∀N > N0,|f(N)| ≥c|g(N)| f(N) = Θ(g(N)), if f(N) = O(g(N))and f(N) = Ω(g(N))

2.2 時間複雜度

時間複雜度,就是描述一個演算法要執行的時間的函數。而我們通常會以「進行基本運算 的次數」來估算。而當我們要計算複雜度時,我們主要關心的是它的成長速度,所我們會 使用前面提及的Big O。

Algorithm 1: Bubble Sort

1 template<typename T>

2 void bubble_sort(T arr[],int n){

3 for(int i = 0; i < n1; i++)

4 for(int j = 0; j < n1i; j++) 5 if(arr[j] > arr[j+1])

6 swap(arr[j], arr[j+1]);

7 }

這是氣泡排序的演算法,當i= 0時,要執行n−1次比較。從i= 0到i=n−2,總共要 執行n(n−1)/2次比較,時間複雜度為 O(n2)。在上方這段程式碼中我們可以發現在最糟 的情況或是已經排好的數列之複雜度皆是O(N2),若是經過一點優化後,最優複雜度就會 是O(N)。

知道了如何估計複雜度之後,要怎麼知道所寫的程式碼是否會在時間內完成而不會

TLE,若你是C或C++的使用者,你可以假設一秒可以進行108 次運算。但如果你的程式

常數很大或很小時,就需要估一下常數的影響。

2.3 空間複雜度

與時間複雜度差不多,可以用來估計一個演算法所需的空間。看一下題目所限制的MB數 就應該能自行估計了。

III

(4)

3 C++ 內建資料結構

3.1 資料結構

資料結構,就是用來儲存資料的結構。陣列就是一種資料結構,但如果遇到一些較麻煩的 題目時,資料結構可以幫助你(有些資料結構不是內建的)。以下就講一些常用的內建資料 結構吧。

這些資料結構在「標準模板庫」(Standard Template Library,簡稱STL)之中,是一個C++

的程式庫。這些都在std底下。

3.2 Iterator

假設現在有個容器C,裡面已經裝了一些資料,如果我要遍歷C中的所有元素,那要如何 做到呢?如果這個容器是set等等它們是沒有下標的,為了處理這個問題,C++STL為每個 容器提供一個成員型別,Iterator(迭代器)。

我們可以用「指標」的概念來理解「迭代器」(實際上,指標算是一種迭代器)。假設現在 有個迭代器it,如果要存取it所指向的內容,那就是在前面加上星號(*it),與指標相同。

以下有迭代器的三種分類:

1. 隨機存取迭代器:這種迭代器能夠和整數的加減法,往後移x項、往前移x項皆可。

當然也可以遞增(++)和遞減(−−),可以把指標當作這種迭代器。

2. 雙向迭代器:只能做遞增(++)和遞減(−−)的運算,也就是後一項和前一項。

3. 單向迭代器:只能做遞增(++)的運算,也就是後一項。

迭代器依照使用方式可分兩種:

1. 輸入迭代器:當要讀取迭代器所指向的內容時,迭代器就為輸入迭代器。所有的迭代 器皆可當作輸入迭代器。

2. 輸出迭代器:當要直接更改迭代器所指向的內容時,迭代器為輸出迭代器。除了常數 迭代器,其他的都可當作輸出迭代器。

C++內建的容器有兩種迭代器,一般的迭代器與逆向迭代器。假設有一個容器型別C為

c,宣告時為C::iterator和 C::reverse_iterator,前者是從前迭代到後,後者是從後迭代到

前。若要表示迭代器的頭尾,前者是c.begin()、c.end(),後者是c.rbegin()、c.rend()。一件 重要的事,*c.end()、*c.rend()是不存在的。

IV

(5)

3.3 vector

vector是動態陣列,也就是說可以自由增加長度,不像一般的陣列在宣告時就確定了。以

下假設變數名為v:

1. 標頭檔:<vector>

2. 建構式:vector<T> v(size_type a,const T& b):一開始v會被a個b填滿,若只有指定 a,那b會是T的預設值。若什麼都沒有指定,v會是一個空的vector。複雜度O(a)。

3. v[i]:v的第i項,複雜度O(1)。

4. v.size():回傳v目前的長度,複雜度O(1)。

5. v.push_back(T a):在v的結尾加一個a,均攤複雜度O(1)。

6. v.pop_back():刪除v的最末項,若v為空,會發生無法預期的結果。複雜度O(1)。

7. v.empty():回傳一個bool,表示v是否為空的,複雜度O(1)。

8. v.clear():清空v,但原本的空間不會被釋放掉。複雜度O(size)。

9. v.resize(size_type a,const T& b):強制將v的長度變為a,若比原本長,則後面加b直

到長度為a,若比原本短則將多出的部分捨去。

10. v.reserve(size_type n):預留放n個T的空間,若n < size,此函數不造成任何影響。

3.4 string

等價於basic_string<char>,string的用法很像vector<char>,但因為字串很常用,所以有經 過一些優化。以下假設變數名為s (t為string):

1. 標頭檔:<string>

2. s=t:讓s變得跟t一樣,複雜度不明,但通常是O(sizes+sizet)。

3. s+=t:在s的尾端加上t,複雜度通常是O(sizes+sizet)。

4. s.c_str():本函式會回傳跟s一樣的C式字串。在C++11中保證複雜度是O(1)。

5. s (比 大 小 或 相 等 的 符 號) t: 回 傳 比 較 s,t 字 典 序 的 結 果。 複 雜 度 通 常 是 O(max(sizes, sizet))。

6. cin>>s:輸入字串至s,直到讀到空白。

7. cout<<s:輸出字串s。

8. getline(cin,s,char c):輸入字串至s,直到讀到字元c。未指定時,c是換行符號。

除了上方所寫的之外,vector有的string都有。

V

(6)

3.5 deque

位 於 <deque> 標 頭 檔。 可 視 為 可 以 在 最 前 面 加 東 西 刪 東 西 的 vector。 設 變 數 名 為 d,

d.pop_front(),d.push_front(a)就是在最前面刪、加東西。但時間和空間複雜度很爛,沒事

不要用deque。

3.6 list

list是個「雙向鏈結結構」,也就是說對於list中的任何一項,都可以O(1)知道它的前一項 和後一項。但若是要存取第i項時的複雜度是O(i)。以下假設變數名為l:

1. 標頭檔:<list>

2. 建構式:list<T> l(size_type a,const T& b):同vector。

3. l.push_front(T a),l.push_back(T a),l.pop_front()„l.pop_back():同deque。

4. l.size():回傳l有幾項,C++11保證複雜度O(1)。

5. l.empty():回傳一個bool,代表l是否是空的。複雜度O(1)。

6. l.insert(iterator it,size_type n,T a):在it指的那項的前面插入n個a並回傳指向a的迭 代器。複雜度O(n)。

7. l.erase(iterator it):把it所指的那項刪去並回傳指向之後那項的迭代器。複雜度O(1)。

8. l.erase(iterator first,iterator last):把[first,last)指到的東西全部刪掉,回傳last。複雜度 與砍掉的數量呈線性關係。

9. l.splice(iterator it,list& x,iterator first,iterator last):first和last是x的迭代器。此函式會

把[first,last) 只到的東西從x中剪下並加到it所指的那項的前面。x會因為這項函式

而改變。若未指定last,那只會將first所指的東西移到it前方。複雜度與轉移個數呈 線性關係。

3.7 Container adaptor

這個東西主要是接收一個容器,然後改造它,成為新的容器。Container adaptor通常不會 有迭代器,以下介紹三個常用的container adaptor。

3.8 stack

可以想像成一疊書,每次只能在最上面放置或拿走一本書。也就是「後進先出」(LIFO)。

stack有兩個型別參數T和C,T是內容物的型別,C是所採用的容器。stack能使用的容

器有vector、deque和list。以下假設變數名為s:

VI

(7)

1. 標頭檔:<stack>

2. 建構式:stack<T,C> s(C& a):s一開始會有一份a的複製品。C預設為deque<T>。

3. s.size(),s.empty():同vector。

4. s.top():存取最後一個進入s的元素,複雜度O(1)。

5. s.push(T a):將一個元素加入s中,複雜度O(1)。

6. s.pop():將最後一個進入s的元素移除,複雜度O(1)。

3.9 queue

可以想像為排隊的人群,有可能是新的一個人來排在隊伍的尾端,或是最前面一個人結完 帳離開隊伍。也就是「先進先出」(FIFO)。queue有兩個型別參數T和C,T是內容物的型 別,C是所採用的容器。queue能使用的容器有deque和list。以下假設變數名為q:

1. 標頭檔:<queue>

2. 建構式:queue<T,C> q(C& a):q一開始會有一份a的複製品。C預設為deque<T>。

3. q.size(),q.empty():同vector。

4. q.front():存取第一個進入q的元素,複雜度O(1)。

5. q.back():存取最後一個進入q的元素,複雜度O(1)。

6. q.push(T a):將一個元素加入q中,複雜度O(1)。

7. q.pop():將第一個進入q的元素移除,複雜度O(1)。

3.10 priority_queue

priority_queue利用幾個內建函式實現binary heap結構,它可以維持最頂的元素永遠是最

大的。priority_queue有三個型別參數T、Con和Cmp。T是內容物的型別,Con是所採用 的容器,Cmp是比大小的依據。priority_queue能使用的容器有vector和deque。Cmp的預 設值是less<T>,此時的priority_queue是最大堆,若改成greater<T>,則priority_queue為 最小堆。以下假設變數名為pq:

1. 標頭檔:<queue>

2. 建構式:priority_queue<T,Con,Cmp> pq:會有一個空的 priotiry_queue。Con 預設為 vector<T>。

3. 建構式:priority_queue<T,Con,Cmp> pq(iterator first,iterator last):會有一個priotiry_queue 內含[first,last)所指到的東西。

VII

(8)

4. pq.size(),pq.empty():同vector。

5. pq.top():回傳pq中最大(最小)的元素,複雜度O(1)。

6. pq.push(T a):將a加入pq中,複雜度O(logsize)。

7. pq.pop():將pq中最大(最小)的元素移除,複雜度O(logsize)。

若你已經確定pq內要放什麼東西,那直接寫在建構式的複雜度比較好,只有O(size),但 如果是一個一個塞進去的話,複雜度是O(sizelogsize)。一般情況下其實沒差,但知道一 下還是比較好。

3.11 pair

pair的目的是把兩個變數綁在一起(可以是不同型別)。以下假設變數名為p:

1. 標頭檔:<utility>

2. 建構式:pair<A,B> p(A a,B b):將A,B兩型別綁在一起,第一項為a,第二項為b。

3. p=s:若s也是同型別,則p會變得跟s一樣。

4. p(比較大小或相等的符號)s:若s也是同型別,則會比較兩者的大小,先比第一項再

比第二項。

5. p.first、p.second:存取第一項、第二項。

make_pair是一個非成員函式。make_pair的好處是不用指明第一項和第二項的型別,用法

是make_pair(A a,B b)函式會回傳一個pair。另外,C++還有<tuple>,可以自行研究。

3.12 set

set實現了紅黑樹,也就是說可以O(logn)插入、刪除或查詢一個值是否在其中。特別的 是,裡面的元素是不會重複的,因此我們會稱元素的值為鍵值(Key)。以下假設變數名為 s:

1. 標頭檔:<set>

2. 建構式:set<K> s:建構一個空的s,複雜度O(1)。

3. s.size(),s.empty():同vector。

4. s.insert(K k):在s中放入一個件值為k的元素,若本來就有,則什麼事都不會做,複

雜度O(logsize)。

5. s.erase(iterator first,iterator last):刪除[first,last),若沒有指定last則只刪除first,複雜 度與刪除的個數呈線性。

VIII

(9)

6. s.erase(K k):刪除鍵值為k的元素,並回傳刪除的個數。複雜度O(logn)。

7. s.find(K k):回傳指向鍵值為 k 的迭代器,若 k 值不存在,則回傳 s.end()。複雜度

O(logn)。

8. s.count(K k):回傳有幾個鍵值為k的元素,複雜度O(logn)。

9. s.lower_bound(K k):回傳迭代器指向第一個鍵值大於等於k的項。複雜度O(logn)。

10. s.upper_bound(K k):回傳迭代器指向第一個鍵值大於k的項。複雜度O(logn)。

3.13 map

map可以O(logn)插入、刪除或查詢一個鍵值對應的值。map有兩個型別參數K和T,K 是鍵值的型別,T是對應的值的型別。map中每一個元素其實是pair<K,T>,所以迭代器 指向的東西是一個pair。以下假設變數名為m:

1. 標頭檔:<map>

2. 建構式:map<K,T> m:建構一個空的m,複雜度O(1)。

3. m.size()、m.empty()、m.erase(iterator first,iterator last)、m.erase(K k)、m.find(K k)、m.count(K k)、m.lower_bound(K k)、m.upper_bound(K k):同set。

4. m[k]:存取鍵值k對應的值,若k沒有對應的值,會插入一個元素,使k對應到預設

值並回傳之。複雜度O(logn)。

5. m.insert(pair<K,T> k):若沒有鍵值為k.first的值,插入一個鍵值為k.first的值對應到

k.second,並回傳一個pair,first是指向剛插入的元素的迭代器、second是true;若已

經有了,回傳一個pair,first是指向鍵值為k.first的元素的迭代器,second是false。

3.14 multiset,multimap

在<set>,<map>中分別有multiset和multimap,與前面的差不多,唯一的差別在於multiset

和multimap 中鍵值可以重複出現,由於multimap中一個鍵值可能對應到不同的值,所

以不支援下標。multiset和multimap有一個函式equal_range(K k),會回傳一個iterator的

pair,第一項代表lower_bound(k),第二項代表 upper_bound(k)。這兩項迭代器之間的項就

是那些鍵值是k的項。set跟map也有這個函式,但其實沒什麼用。

3.15 unordered_(multi)set,unordered_(multi)map

unordered_(multi)set和unordered_(multi)map 優化掉set和mapO(logn)的複雜度,這兩個 分別在在標頭檔<unordered_set>和<unordered_map>詳細的可以自己查,以下只寫一些重 點:

IX

(10)

1. unordered系列的迭代器為單向迭代器。

2. unordered系列沒有將所有項依鍵值排序(這也是它名字的由來),因此迭代器在遍歷

容器時不會依鍵值的大小順序遍歷。

3. 因為沒有排序,所以當然沒有lower_bound、upper_bound。

4. 比起(multi)map和(multi)set,unordered系列的期望複雜度少一個log。

在宣告變數(建構式)時,你可以指定bucket至少有幾個。如果鍵值是內建型別而且你沒

有指定bucket至少有幾個,那麼它會自己幫你搞定。

3.16 bitset

你可以將它想像為一堆 0和1 的陣列,也就是說bitset大小是固定的。但每一項只用到 1bit的空間,並且bitset的位元運算是被優化過的,對常數優化及空間壓縮有不錯的效用。

以下假設變數名為b:

1. 標頭檔:<bitset>

2. 建構式 bitset<N> b(a):用a 初始化一個長度為 N 的bitset。這裡 a 可以是unsigned

long、string或C式字串。如果沒有指定a,或者如果b有一些地方沒被a初始化,那

些地方預設為0。

3. b.count():回傳b有幾個位元是1。複雜度O(N)。

4. b.size():回傳b有幾個位元。複雜度O(1)。

5. b(位元運算):不管是一元還是二元的位元運算都可以。如果是兩個bitset的二元位元

運算,兩個bitset的長度需一致。複雜度O(N)。

6. b[a]:存取第a位。複雜度O(1)。

7. b.set():將所有位元設為1。複雜度O(N)。

8. b.reset():將所有位元設為0。複雜度O(N)。

9. b.flip():將所有位元的0、1互換(反白)。複雜度O(N)。

10. b.to_string():回傳一個字串和b的內容一樣。複雜度O(N)。

11. b.to_ulong():回傳一個unsigned long和b的內容一樣(在沒有溢位的範圍內)。複雜度

O(N)。

bitset不是容器,而且它也沒有迭代器。通常而言,如果要估計常數的話,相較於直接使

用陣列,空間是1/8、count約是1/6、位元運算約是1/30。當然這些都不是絕對的。

要注意的是,上述的複雜度沒有明文規定,不過通常是如此。

X

(11)

3.17 習題

知道這些還不夠,要實際練習才有用。

1. (ZJ b298)(vector)

2. (ZJ b291)(vector,map,string) 3. (UVa 514)(ZJ c123)(stack) 4. (TIOJ 1993)(bitset)

5. (TIOJ 1613)(priority_queue) 6. (TIOJ 1809)(set)

7. (TIOJ 1168)(priority_queue,bitset/vector)

4 <algorithm>

C++裡給了一個標頭檔<algorithm>,裡面有很多種函式,但這些函式並不知道它會接收 到的東西長怎樣,此時迭代器就發揮了功勞。

以下大約介紹一些常用的函式:

4.1 排序

1. sort(iter first,iter last,Cmp cmp):將 [first,last)依照 cmp排序,使得若a嚴格地在b前

面,則 cmp(a,b)為真。這裡要求iter是隨機存取迭代器,而cmp是一個接受兩個參

數,回傳一個 bool 的函數指標或函數物件。不指定cmp時會由小排到大。複雜度 O(Cnlogn),其中C是cmp的複雜度。

2. stable_sort:跟sort一樣,然而保證如果有兩項a跟b的值一樣且a一開始在b前面,

那麼最後a也會在b前面。一般來說,有額外空間的話時間複雜度O(nlogn)、額外 空間複雜度O(n)。但是如果沒辦法找到額外的空間,時間複雜度會變成O(nlog2n)。

3. nth_element(iter first,iter nth,iter last,Cmp cmp):將排序後應該會在第nth位置的元素x 移到第nth個位置,並且讓應該在x前面的所有元素都在x前面,反之亦同。參數要

求同sort。平均複雜度O(n),但未保證最差複雜度。

XI

(12)

4.2 對付已排序序列的函式

1. lower_bound(iter first,iter last,T t,Cmp cmp):跟 set的lower_bound是一樣的意思。若 iter是隨機存取迭代器,複雜度 O(logn)。若不然,複雜度O(n)。如果iter是set和 map系列的迭代器,請直接採用set和map的成員函式。

2. upper_bound(iter first,iter last,T t,Cmp cmp):跟set的upper_bound是一樣的意思。

3. equal_range(iter first,iter last,T t,Cmp cmp):回傳一個pair,第一項是lower_bound,第 二項是upper_bound。

4. merge(iter1 first1,iter1 last1,iter2 first2,iter2 last2,iter3 res,Cmp cmp):假設 [first1, last1)

和[first2, last2) 是兩個經cmp排序好的序列,將兩個序列合併並排序,輸出至以res

為首的序列並回傳指向序列末端的迭代器。記得讓res後面有足夠的空間。複雜度線 性。

5. set_union(iter1 first1,iter1 last1,iter2 first2,iter2 last2,iter3 res,Cmp cmp):假設A=[first1,last1)

和B=[first2, last2)是兩個經cmp排序好的序列,將兩個序列取聯集排序輸出至以res

為首的序列並回傳指向序列末端的迭代器。其餘同merge。

6. set_intersection:同set_union,但此函式取的是交集。

7. set_difference:同set_union,但此函式取的是餘集(AB)。

8. set_symmetric_difference:同set_union,但此函式取的是對稱餘集(A∪B−A∩B)。

4.3 字典序

1. next_permutation(iter first,iter last,Cmp cmp):將[first,last)變成原本的某個排序,使得 字典序是比原本大的所有排序中最小的。如果成功了,這個函式會回傳true;否則回

傳false,並將[first,last)變成字典序最小的那個排序。

2. prev_permutation(iter first,iter last,Cmp cmp):將[first,last)變成原本的某個排序,使得 字典序是比原本小的所有排序中最大的。如果成功了,這個函式會回傳true;否則回

傳false,並將[first,last)變成字典序最大的那個排序。

4.4 習題

1. (ZJ a233)裸排序題

2. (TIOJ 1617)(IOI 2000)(Interactive)(nth_element)

3. (TIOJ 1907)LIS最長的嚴格遞增子序列(sort,upper_bound)

XII

參考文獻

相關文件

假設檢定的檢定統計量是用來評估虛無假設和對立假設的資料 摘要。 p-value 是假設虛無假設為真,且在這個假設下決定檢定統計量 的值會與對立假設的方向一樣極端,甚至更極端。 Pooled assumes equal population variances Data Confidence Interval Estimate Hypothesized

種情形似應列入給分與否的討論。再者若能將題幹敘述改為「將相同體 積的上述兩種溶液與少量澱粉溶液混合」,似乎較無疑慮。 非選擇題第二題 4 二、去年(2016)諾貝爾化學獎,頒給分子機械的研究。它是最小機械的極致,將 使奈米機器人成為可行,例如分子肌肉需要一個可擴張和收縮的結構,並且這 兩種結構可以選擇性地轉換,類似於肌肉的功能。