「圖是圖論的主要研究對象。圖是由若干給定的頂點及連接兩頂點的邊所構成的圖形,這種圖形通常用來描述某些事物之間的某種特定關係。頂點用於代表事物,連接兩頂點的邊則用於表示兩個事物間具有這種關係。」
以上是取自維基百科的說明。在資訊的講法上,一張圖(Graph)通常包含著點(Vertex / Node)和邊(Edge)。另外,我們通常會把一條邊表示成。
無向圖(undirect graph):圖中的任何邊與等價,意指你可以從走到也能走回來。
有向圖(direct graph):圖中的邊,通常邊代表這條邊允許從走到,但不能反向走。
此處說明皆以下圖為無向圖範例。
此處說明皆以下圖為有向圖範例。
點數、邊數:以和表示。為求方便書寫,在寫複雜度時通常以、表示。
有向圖範例中,。
權重(weight):有時候我們會在每一個點和邊附帶一個數稱為「權重」。比較常見的是邊的權重(邊權),通常會作為表達長度或花費時間的方法。
有向圖範例中,邊的邊權為2。
度(degree):一個點連接的邊數即稱為這個點的「度」。在有向圖中可細分為「入度(in-degree)」和「出度(out-degree)」,分別表示以這個點為終點和起點的邊的數量。
無向圖範例中,點1的度數為3。
有向圖範例中,點1的入度為1,出度為2。
相鄰(adjacent):無向圖中,兩個點相鄰代表存在一條邊。
無向圖範例中,點1和點3相鄰,點1和點6不相鄰。
指向(consecutive):有向圖中,指向代表存在一條邊。
有向圖範例中,點1指向點2,點2不指向點1。
路徑(path):一個由走到的路徑,可以經由一連串的點由指向或相鄰關係構成。
無向圖範例中,點3到點6存在路徑3→1→2→6。
有向圖範例中,點3到點6存在路徑3→1→2→6,點6到點3不存在路徑。
行跡(trace):一條路徑中所有的邊皆不同,稱為行跡。
無向圖範例中,路徑2→1→4→3→1為行跡。
有向圖範例中,路徑1→4→3→1→2為行跡。
簡單路徑(track):一條路徑中除了起點終點外,所有的點兩兩相異,稱為簡單路徑。
無向圖範例中,路徑2→1→4→3為簡單路徑。
有向圖範例中,路徑1→4→3為簡單路徑。
環(cycle):一條簡單路徑的起終點相同,稱為環。
無向圖範例中,路徑2→1→4→3→1→2為環。
有向圖範例中,路徑1→4→3→1為環。
迴路(circuit):一條行跡的起終點相同,稱為迴路。
無向圖範例中,行跡1→3→4→1為迴路。
有向圖範例中,行跡1→4→3→1為迴路。
自環(loop):一條邊滿足即稱為自環。
無向圖及有向圖範例中,邊為自環。
重邊(multiple edge):在一張圖中,存在滿足且,則稱為重邊。
無向圖及有向圖範例中,邊為重邊。
連通(connected):無向圖中,若和存在路徑,則和連通。若一群點倆倆連通,則這些點都連通。
無向圖範例中,任兩點都連通。
簡單圖(simple graph):圖中不存在自環及重邊即稱為簡單圖。
連通圖(connected graph):在無向圖上,如果任兩個點都可以經由一些邊互相訪問,那這張圖就是連通圖。
樹(tree):一張沒有環且連通的圖稱為一棵樹。
森林(forest):一張由樹棵不連通的樹組成的圖稱為森林。
完全圖(complete graph):無向圖上,對於任何一個點對,都存在邊。一張個點的完全圖通常會以簡記之。完全圖在集合上被稱為「團(clique)」。
競賽圖(tournament graph):有向圖上,對於任何一個點對,都存在邊。
有向無環圖(directed acyclic graph,簡稱DAG):沒有環的有向圖稱為一張DAG。
二分圖(bipartite graph):如果你能把這張圖的點分成兩個集合,且對於任何的邊都滿足分別在不同的集合裡,那這張圖即為二分圖。
平面圖(planar graph):可以畫在平面上並且使得不同的邊可以互不交疊的圖。
子圖(subgraph):如果兩張圖;滿足且則稱是的子圖。
補圖(complement graph):令為一張圖,包含所有的二元子集(2-element subset)。則圖是的補圖。簡單來說就是若原本的邊消失,原本不存在的邊出現。
同構(isomorphic):如果對於兩張圖、的點集、,存在一個一一對應的函數,使得對於中的任何兩點、,與相鄰若且唯若與相鄰,稱與同構,記為。簡單來說就是這兩張圖可以透過重新編號來變成長得一樣的圖。
生成樹(spanning tree):無向圖中,一個樹的子圖稱為一棵生成樹。
vector
),第個裡面放所有第個點指向的點的編號和其他資訊。空間複雜度,加邊時間複雜度,刪邊時間複雜度。鄰接串列的好處在於,遍歷圖的時候可以保證不會讀到不必要的資訊,複雜度通常都是,空間也相對優秀。其餘情況用鄰接矩陣皆不會比較差。
遍歷是指存取圖中資料的方法。
以下的範例程式碼中,我們以鄰接串列vector<int> G[]
來儲存圖片。
簡稱DFS。找到與當前的節點相鄰的未尋訪節點進行遍歷。若此點沒有新的鹿,則沿原路返回直到到達最近一個與未遍歷過的節點相鄰的節點,再向下遍歷。
以下為遞迴版本的DFS,時間複雜度為,額外空間複雜度為。
以下為STLstack
版本的DFS:
簡稱BFS。先遍歷過當前節點的所有相鄰節點,再將這些點的相鄰節點放入待遍歷的佇列中。時間複雜度一樣為,額外空間複雜度為。
以下為用queue
實作的BFS:
可以發現BFS會先把距離起點為0的點(起點本身)遍歷過,再遍歷距離起點為1的點,再遍歷距離為2的點,依此類推。因此,我們可以用BFS解決邊權相同的最短路徑問題。
當我們在二維表格上遍歷時,如果用8行程式控制移動的8個方向時會使程式可讀性降低,也提高錯誤率。
我們可以用以下的小技巧來加快寫程式的速度:
在圖論中,樹是一種有很多特殊性質的圖,在樹上可以有很多的特別算法和特別的問題。
回顧一下樹的定義:一張沒有環且連通的圖稱為一棵樹。
對於以下任何一種無向圖的敘述皆是在表示樹,這些敘述也可以當作是判斷一棵樹的方法。
這些性質甚至有時候會被當作是解題的關鍵,可以嘗試理解它們。
此處說明皆以下圖為範例
根(root):樹上一個具代表性的節點。若題目有給定根結點,則稱這棵樹為「有根樹」。根通常會被當成遍歷起點,所以無根樹通常也得找一個節點給它。
此處範例皆以節點1為根結點。
葉子(leaf):在樹上度數只有1的節點稱為葉子,通常如果是有根樹根節點的度數是1的話會獨立不將其視為葉節點,視題目所需。
範例圖中,節點4、5、8、11、12、13為葉節點。
父節點(parent)/子節點(child):有根樹中,兩個相鄰的節點,離根較近的節點是另一個節點的父節點,反之則為子節點。
範例圖中,節點3為節點9、10的父節點;節點9、10為節點3的子節點。
祖先(ancestor)/子孫(descendant):有根樹中,一個節點的祖先包含了它的父節點和其父節點的祖先。反過來說,對於一個節點的祖先,這個節點為它的子孫。
範例圖中,節點2為節點5、6、7、8的祖先;節點5、6、7、8為節點2的子孫。
距離(distance):兩點間的距離是它們之間的路徑的邊數或邊權總和。
範例圖中,節點7和節點12的距離為6。
深度(depth):有根樹中,一個點的深度是它到根的距離。
範例圖中,節點7的深度為3。
高度(height):有根樹中,一個點與它的子樹中距離最大的葉節點的距離稱為高度。根的高度稱為這整棵樹的高度。
範例圖中,節點1(根節點)的高度為4,節點3的高度為2。
子樹(subtree):移除一個點後,原樹會被拆成很多棵樹,稱為子樹。有根樹中,一個節點的子樹通常泛指它的所有子孫,包含自己。
範例圖中,節點3、9、10、11、12、13為一棵子樹。
二元樹(binary tree):每個節點都至多只有2個子節點的樹。
下圖為一棵二元樹。
二元樹又比樹多了些性質。
一棵二元樹的遍歷分為「前序(pre-order)」、「中序(in-order)」、「後序(post-order)」三種,三者的差別為造訪根節點的時機。
實作上我們以DFS的方式,再藉由改變遍歷根節點的時機仿造出三種不同遍歷的方式。
以下為範例程式:
對於一棵二元樹,只要有其中序及前序或後序遍歷的順序即可還原出原本的二元樹長相。利用中序的「根節點位置的左右恰為左右子樹」的性質搭配分治法處理,複雜度為。
二元搜尋樹是二元樹的資料結構應用。其定義如下:
對於任何一個序列,只要依照上面兩條規則就能蓋出對應的二元搜尋樹。而對於一個已排序的序列,其二元搜尋樹會是一條鏈,蓋樹的時間複雜度會退化至,深度變成。所以二元搜尋樹本身並不是很實用,真正實用的是其推廣結構。
要找出樹直徑,可以從任意一點開始進行DFS,並從距離最遠的點再DFS一次,得到距離點最遠的點。則到的路徑即為樹直徑。
而樹圓心會是直徑上的某個節點,所以可以在第二次的DFS中一起算出。
要找出樹重心,可以先以任意一點為根,進行一次DFS後算出以各節點為根的子樹大小。
此時可以得知,當點為根時,新的樹的最大子樹大小會是點的所有子節點中子樹最大者以及節點總數減去子孫總數再減1的最大值。算出每個節點的值後,即可得到此樹的樹重心。
樹重心有以下性質:
記錄一個有根樹遍歷時,設進入節點的時間為,離開的時間為,可觀察到以下性質。
故時間戳記基本上可以先被認為是判斷祖先子代關係的工具。
簡稱LCA。在一棵有根樹上兩節點的最低共同祖先即為其祖先的交集中,深度最深的祖先。
暴力搜尋LCA的複雜度為,不甚理想。使用倍增法可以大幅提升尋找LCA的效率。
我們可以事先存好每個節點往上層的祖先。
建表複雜度為。因為層數不會超過層,故,複雜度可寫成。
可以知道若點的第層祖先是的祖先,則點的第層祖先也是的祖先。我們可以對這個性質進行二分搜。若查詢的左右界為,我們就能確定每次查詢的中間值都會在我們建好的表上了。
以下範例中ancestor(a, b)
回傳true
若點為點的祖先;反之回傳false
。可以用時間戳記的方式預處理,並以時間查詢。
簡稱MST。無向圖中,一個樹的子圖其包含原圖的所有節點稱為一棵生成樹。生成樹有許多長相,包含DFS或BFS的路徑其實都可以構成一棵生成樹。在所有的生成樹中,邊權總和最小的生成樹即為最小生成樹。
並查集是一種資料結構,它可以支援以下操作:
這裡的集合在圖論上通常會被當成「連通塊」,這也代表著並查集擁有查詢任兩點是否連通的功能。
並查集對於每個集合賦予一個「領導者」,每個元素只要找到領導者就可以知道所在的集合了。
實作上,由於一開始每個元素都尚未合併,都是自己一個集合,所以我們會令group[i] = i
,表示集合的領導者是自己。合併時只要在兩個集合的領導者間連一條邊即可。
實作並查集時,通常需要兩個函數,功能分別為尋找某元素隸屬的集合的領導者及合併兩集合。
以下範例中,group[i]
指的是元素i
隸屬的集合的領導者,而sz[i]
指的是元素i
隸屬的集合的大小,此陣列的值只對目前集合的領導者有效。
我們將原圖中連接一部分點的生成樹稱為最小生成子樹,可以發現一些特性:
所以可以用貪心法,從最小權重的邊開始選起。若目前權重最小的邊其兩個端點隸屬於不同的集合,則選取該條邊,也就是連接兩棵最小生成子樹;若兩個點隸屬同一集合,也就是兩點間已連通,則不選擇該條邊。
先對每條邊以邊權排序後,即可依上述的做法找出最小生成樹。複雜度,其中為阿克曼函數的反函數,成長速率極慢,幾乎可視為常數。
另外,最小生成樹還有Prim's algorithm及Borůvka’s algorithm,兩者較不常見,在此先不說明。
枚舉不在生成樹中的邊,放入最小生成樹後再拔掉最小生成樹中、之間路徑的邊權最大的邊,取最小值,即為次小生成樹。理論上複雜度會是,可以再優化。
觀察:擺一條新的邊進去一棵樹,必定會在、之間形成一個環。所以只要拆掉除了以外這個環上權重最大的邊,就能形成新的、對於這條邊次小的樹。而環上權重最大的邊即為、間路徑的最大值,故我們能用LCA來尋找這條邊。總複雜度為。
最短路徑即為兩點間所有路徑中邊權和最小者。最短路徑上不允許存在負環,否則可以經由不斷通過負環讓路徑權重和持續變小。
即求出一個點到所有點的最短路徑。
對於任意兩個點、,如果起點到它們的距離、滿足,那我們就可以把更新成,使到的距離變短,這個動作就叫鬆弛。
把起點的最短路徑設為0,不斷枚舉所有邊,嘗試每條邊是否能進行鬆弛,直到所有邊都無法進行鬆弛為止。每次鬆弛複雜度為。
因為最短路徑不會通過相同的點兩次,所以在沒有負環的情況,至多只要鬆弛次就可以得到最短路徑。至多總共要鬆弛次,故總複雜度為。
若在第次時還有邊能進行鬆弛,表示此圖有負環。故Bellman-Ford也可當作是判斷負環的方法。
如果邊權都是非負實數,會存在一個性質:用完成一部分的最短路徑樹上的點去鬆弛其他不在樹上的點,所得到的離根最近的點肯定也在最短路徑樹上。
我們可以利用此性質,先將起點的最短路徑設為0,每次選擇當前尚未放入最短路徑樹的點中路徑長度最小的,該點的最短路徑即確定。接著再對該點連出的每條邊進行鬆弛,直到所有點都在最小路徑樹為止。
最糟情況下每條邊都需被鬆弛一次,複雜度。
請注意,此作法僅限於邊權皆為非負實數使用。
一口氣求出所有點對的最短路徑,亦可使用次單點源最短路徑來完成這件事。
Floyd-Warshall使用動態規劃計算多點源最短路徑。
令代表只以點1到點為中繼點的情況下到的最短路徑長。時間複雜度為,空間複雜度可藉由滾動陣列壓縮到。其實作容易,常數也很小。
若執行完畢後出現的情況,表示存在負環。故Floyd-Warshall也是一個判斷負環的方法。
此份講義中所有圖皆使用Draw Graph製作
2019師大附中資訊校隊培訓