本週延續前兩週,將繼續介紹資料結構以及 STL 的使用
並且接下來將使用這些基本資料結構構造(解釋)一些演算法
底下的術語挺多的,各位不須馬上就得記起來,等到未來碰到再回來多複習幾遍
圖 (Graph),是一個由邊 (Edge) 集合與點 (Vertex) 集合所組成的資料結構。
Graph 的術語:
在討論圖的邊,常會有 是邊起點與 是邊終點的慣例用符
上面這就是一種有向圖
通常 graph 用鄰接表 (adjacency list) 或鄰接矩陣 (adjacency matrix) 儲存資料
為每個點紀錄其鄰點
為每對點紀錄邊的關係
使用鄰接矩陣要注意空間成本
樹 (Tree),這個資料結構在圖像化看起來像顆倒掛的樹,根在上,而葉子在下。
Tree 的術語及特點:
考慮設計一個結構,
它要能存放一些集合,且這些集合之間沒有相同元素
這樣的集合族稱作 Disjoint sets
Disjoint sets 通常應用在"分類"問題中
直觀的想法是將每個集合有哪些元素,用陣列或連結串列紀錄起來
而常見的集合操作有,新增、刪除、(取)聯集、取交集(?)、取集合大小
可以思考一下這些操作的複雜度要多少
但併查森林則是將紀錄方式從 "集合有哪些元素" 改為 "元素屬於哪個集合"
Find 會尋找某個元素屬於哪個集合
假設有元素 ~ ,其中 一組, 一組。
下 Find(5)
指令,那麼他要回傳給我
稍微想像一下可發現,若樹長這樣:
那麼明顯 Find(i)
的複雜度為
直覺的,如果樹不是長得這麼長,而是一個個節點都直接接在 底下
那麼 Find(i)
的複雜度似乎就能下降了。
所以每當回溯時就順便把最上層 group 的標號[3](也就是 ) 給所有拜訪完的節點
也就是改寫為:
於是原本的森林下 Find(5)
指令
森林會變成:
Union 會將兩個集合合併起來 (再次提醒:此集合族是 disjoint 的)
若對下圖這樣的情況,做 Union(4, 2);
也就是將 的 root 合併到 的 root
則上圖會變為下圖這樣:
還有種方式稱作 Union by rank/size,將 rank/size 小的樹合併到 rank/size 大的樹下,可以加快許多。
std::stringstream
是很方便的東西,不過據說效率不夠優。
UVa OJ 10583 Ubiquitous Religions
UVa OJ 11987 Almost Union-Find
CODEFORCES 1253D Harmonious Graph
TIOJ 1192 鑰匙設計
有了圖,有了樹,可以開始討論這回事了
不彷將搜尋所能觸及到的 可能性/目標 稱為狀態
每當狀態改變後,前個狀態到下個狀態的過程稱狀態轉移。
若把狀態看作點,而狀態轉移看作邊,包含這些點與邊的圖稱作狀態空間
回憶一下上面曾介紹的術語:
遍歷也是種搜尋,但"走完"可能得付出龐大的時間成本,或是空間成本
根據狀態空間規模,須用一些手段使得能更快速的找到所想要的東西。
本章使用的圖論符號慣例:
深度優先搜尋 (Depth-first Search/DFS):走訪為每拜訪一節點就往其一鄰點拜訪下去。
這裡的走訪為走遍所有點(而非邊),若中途碰到曾走過的點不往下繼續走。[4]
按照上圖,走訪順序為 依照自然數列開始走訪到 。
DFS 走過的道路為樹,稱此樹為 DFS 樹:
(圖上節點左上角的數字代表深度)
這裡 for 迴圈採用 Range-based 寫法
其中 vis[i]
[5] 為 true
代表此節點已拜訪過,下次不考慮此節點為更深的子孫節點。
所以在開始進行走訪前,將起點設為已拜訪:
DFS 除了能夠以上述遞迴方式呈現,也可以採用 stack 來實作:
搜尋某個狀態,可以利用函式與參數表示,例如會把 f(1, 2, 3)
和 f(3, 4, 5)
這樣的函式呼叫,當作不同的狀態去接觸(求解)它。
題目要求一個區域中有幾個連通圖
所謂的連通,就是圖中任意兩點間至少有一條路徑
當接觸到 dfs(r, c)
這個狀態時,代表這裡有塊包含座標 的 oil deposit (前提是 plot[r][c]
為 '@'
)
而 DFS 走訪時,只需確保不再重複走到走過的點,所以走過就設 '*'
只要是連通圖,DFS 都能把此圖走訪完
這裡簡單算走進幾次連通圖就好
廣度優先搜尋 (Breadth-first Search/BFS):走訪為每拜訪一節點就將其全部鄰點拜訪過。
按照上圖,走訪順序為 依照自然數列開始走訪到 。
當然,與 DFS 同樣,因為走訪中途碰到曾走過的點不往下繼續走,所以 BFS 走完後也會有個 BFS 樹:
搜索地圖起點到任意點的最短步數,
例如地圖上 *
代表牆(不能走),$
代表路,%
是起點,@
是終點
且每一步只走上下左右一格:
BFS 可以應用在這[7]:
其中上面數字代表深度。
這個走法就跟粘菌走迷宮同樣
下方給出實作程式碼,可以配合上面例子來理解:
根據條件應將不合法的走法濾掉,在 Q.push()
之前可判斷一下。
BFS 跟 DFS 結構只差在 stack 和 queue,除此之外兩者是非常相似的
一開始先將 Joe 與各火點放進 queue 中,以便讓 BFS 以此為起點走訪:
(其中 INF
[5:1] 為一個非常大的數字,例如 int
的上限)
Joe 不能被火燒到,所以 Joe 一定要走得比火快
由此,算出各點何時火會燒過來就能判斷 Joe 是否能比火先到
搜尋火到各點的最短路:
其中 point
結構三個變數為 row, column 與 time (火抵達的時間)
並利用 dr 與 dc 以當前所在點走遍四個方向:
現在 maze (也就是地圖) 上有紀錄火到的時間了。
接下來讓 Joe 去尋找最短路:
j.t + 1 >= maze[nr][nc]
就能看 Joe 走這點是不是會被火燒
最後判斷走到邊界,就成功逃脫了!
UVa OJ 532 Dungeon Master
STEP5 0127 攻略妹妹
UVa OJ 11234 Expressions
UVa OJ 1599 Ideal Path
利用各種可得的限制來做搜尋目標中的偷吃步
八皇后問題[8]:西洋棋盤上任意擺放八個皇后彼此都不互攻的情況有幾種?
如下圖是其中一種合法的擺法
[8:1]
若想著把每一種任意擺放可能性列出來,再來挑選可行的盤面,
將有 [9] 種盤面要產,明顯的程式會跑很久
而兩個皇后放在同個 row 或 column 上一定會互攻,所以只需在每個 row 或 column 擺放一個皇后就好:
這邊的 check(r, c)
就是本節的主題了,
在轉移狀態(盤面)前,若能預感(?)這狀態不是想要的,就中斷轉移,然後 backtrack 到原狀態,繼續進行別的狀態轉移
用 check(r, c)
檢查將皇后放置在 後是否能繼續再放置其他皇后。
有點幾何知識的話,會發現 check()
只需要 就能做到:
枚舉的盤面會少於 很多,因為 check()
剪掉了許多不必再繼續遞迴下去的 DFS 樹枝。
UVa OJ 524 Prime Ring Problem
UVa OJ 211 The Domino Effect
STL 全名 Standard Template Library
由容器 (containers)、迭代器 (Iterators)、演算法 (Algorithms)、函式 (Functions) 4 種元件組成。
延續第三週,我們將再介紹幾個常用的 STL 裡的容器
絕大部分 STL 的東西只要涉及區間的操作,區間表示一律為左閉右開
推薦的參考網站: cplusplus.com、C++ reference
介紹 std::set
之前,得先介紹一些集合操作
inserter 顧名思義,就是把一些東西插入到某個地方
union 就是將兩個集合取聯集
取交集
差集
集合(set) 是非常基礎的數學結構,對於資料結構也一樣基礎
特別注意:集合中的元素不會重複
std::set
std::set
是使用紅黑樹實作的,插入、刪除、查詢都為 ,其中元素也不會重複。
且元素們在 set
容器中保持排序的
也就是說迭代器位置會優先從元素值小的排到大的
set<T> S
: 一個空的集合
S.insert(T a)
: 插入元素 a
S.erase(iterator l, iterator r)
: 把 位置上的元素移除
S.erase(T a)
: 移除元素 a
S.find(T a)
: 指向元素 a
的迭代器;若 a
不存在,則回傳 s.end()
S.count(T a)
: 元素 a
是否存在
鍵(key)值(value)對(pair) 是非常實用的資料結構
例如想表達每個人 的身高 可以寫:
又或是表達有理數 的分子分母:
std::map
std::map
的插入、刪除或查詢為
map
的每個元素結構為 std::pair<T1, T2>
構成的 KVP:
map<T1, T2> M
: 空的
M[k] = a
: 修改鍵值 k
對應的值為 a
M[k]
: 存取鍵值 k
對應的值
M.insert(pair<T1, T2> P)
: 插入一個鍵值對 P
每次將 skill 為 student 附近相差 以內的 skill 都記錄下來就行
例如有 skill 、 及 的 student 存在
那麼首先記錄 :
接著
然後
所以每次對於 skill :
接著找出哪個區間記錄值最大:
且慢!
注意到一個限制:
若把 cnt
陣列的空間開得這麼大,明顯的會 Runtime error[11]
回憶一下第一場比賽
所以把陣列改成 map<int, int>
,就能避免空間上的龐大需求
UVa OJ 11991 Easy Problem from Rujia Liu?
優先隊列 (priority queue) 是隊列 (queue) 的一個變種
std::priority_queue
每次進出的時間複雜度都為
對於數值型態的元素,數值越大優先度越大
對於未定義順序關係的元素(型態),要先定義小於運算子,例如:
priority_queue
也能定義比較函數,可以自行實驗
樸素的,將所有 binary string (preferences) 都產出來,並把 string 對應的抱怨 (complaint) 算出來
接著找出最小抱怨並且不屬於 forbidden types 的 binary string 就好。
span
保存將產出的 binary string:
以 seed
作為基礎,再將 string 加上 "0"
或 "1"
並每次將當前的抱怨值算出後保存:
其中 one[i]
與 zero[i]
為 Shakti 的朋友們對於第 個 option 的加總 (選項只有 和 )
但光是產出所有 binary string 就足夠讓複雜度導致 TLE。
在上述過程中,若把某些 string 去除掉,就能使效率增加不少
合理的,答案只要最小抱怨值的 binary string,那在過程中移除抱怨值較大的那些 string 就行了:
因為共有 個 forbidden type,所以至少要保存 個 binary string
最後把 string 為 forbidden type (ban
) 的濾掉,留下抱怨值最小的就好:
ICPC 3135 Argus
UVa OJ 11997 K Smallest Sums