【程序人生】數據結構雜記(六)
說在前面
個人讀書筆記
圖的概念
圖結構是描述和解決實際應用問題的一種基本而有力的工具。
所謂的圖(graph),可定義爲。其中,集合中的元素稱作頂點(vertex);集合中的元素分別對應於中的某一對頂點,表示它們之間存在某種關係,故亦稱作邊(edge)。
一種直觀顯示圖結構的方法是,用小圓圈或小方塊代表頂點,用聯接於其間的直線段或曲線弧表示對應的邊。
從計算的需求出發,我們約定和均爲有限集。
無向圖、有向圖及混合圖
若邊所對應頂點和的次序無所謂,則稱作無向邊(undirected edge),例如表示同學關係的邊。
反之若和不對等,則稱爲有向邊(directed edge),例如描述企業與銀行之間的借貸關係,或者程序之間的相互調用關係的邊。
如此,無向邊也可記作,而有向的和則不可混淆。這裏約定,有向邊從指向,其中稱作該邊的起點(origin)或尾頂點(tail),而稱作該邊的終點(destination)或頭頂點(head)。
若中各邊均無方向,則稱作無向圖。若中只含有向邊,則稱作有向圖。若同時包含無向邊和有向邊,則稱作混合圖。
相對而言,有向圖的通用性更強,因爲無向圖和混合圖都可轉化爲有向圖:
每條無向邊都可等效地替換爲對稱的一對有向邊和。
度
對於任何邊,稱頂點和彼此鄰接(adjacent),互爲鄰居;而它們都與邊彼此關聯(incident)。
在無向圖中,與頂點關聯的邊數,稱作的度數(degree),記作。
以上圖中無向圖爲例,頂點{ A, B, C, D }的度數爲{ 2, 3, 2, 1 }。
對於有向邊,稱作的出邊(outgoing edge)、的入邊(incoming edge)。的出邊總數稱作其出度(out-degree),記作;入邊總數稱作其入度(in-degree),記作。
在上圖中的有向圖中,各頂點的出度爲{ 1, 3, 1, 1 },入度爲{ 2, 1, 2, 1 }。
簡單圖
聯接於同一頂點之間的邊,稱作自環(self-loop)。在某些特定的應用中,這類邊可能的確具有意義——比如在城市交通圖中,沿着某條街道,有可能不需經過任何交叉路口即可直接返回原處。
不含任何自環的圖稱作簡單圖(simple graph)。
通路和環路
所謂路徑或通路(path),就是由個頂點與條邊交替而成的一個序列,且對任何都有。也就是說,這些邊依次地首尾相聯。其中沿途邊的總數,亦稱作通路的長度。
儘管通路上的邊必須互異,但頂點卻可能重複。沿途頂點互異的通路,稱作簡單通路
特別地,對於長度的通路,若起止頂點相同(即),則稱作環路(cycle),其長度也取作沿途邊的總數。同樣,儘管環路上的各邊必須互異,但頂點卻也可能重複。反之,若沿途除外所有頂點均互異,則稱作簡單環路。
特別地,經過圖中各邊一次且恰好一次的環路,稱作歐拉環路(Eulerian tour)——其長度恰好等於圖中邊的總數。經過圖中各頂點一次且恰好一次的環路,稱作哈密爾頓環路(Hamiltonian tour)。
不含任何環路的有向圖,稱作有向無環圖
帶權網絡
圖不僅需要表示頂點之間是否存在某種關係,有時還需要表示這一關係的具體細節。以鐵路運輸爲例,可以用頂點表示城市,用頂點之間的聯邊表示對應的城市之間是否有客運鐵路聯接;同時,往往還需要記錄各段鐵路的長度、承運能力,以及運輸成本等信息。
爲適應這類應用要求,需通過一個權值函數,爲每一邊指定一個權重(weight),比如即爲邊的權重。各邊均帶有權重的圖,稱作帶權圖(weighted graph)或帶權網絡(weightednetwork),有時也簡稱網絡(network),記作。
圖ADT的實現方法
鄰接矩陣
鄰接矩陣(adjacency matrix)是圖ADT(Abstract Data Type)最基本的實現方式,使用方陣表示由個頂點構成的圖,其中每個單元,各自負責描述一對頂點之間可能存在的鄰接關係,故此得名。
對於無權圖,存在(不存在)從頂點到的邊,當且僅當。上圖中和即爲無向圖和混合圖的鄰接矩陣實例。
對於帶權網絡,如上圖中所示,矩陣各單元可從布爾型改爲整型或浮點型,記錄所對應邊的權重。對於不存在的邊,通常統一取值爲無窮大或0。
鄰接表
以如上圖中所示的無向圖爲例,只需將如上圖中所示的鄰接矩陣,逐行地轉換爲如上圖中所示的一組列表,即可分別記錄各頂點的關聯邊(或等價地,鄰接頂點)。這些列表,也因此稱作鄰接表(adjacency list)。
圖遍歷算法
無論採用何種策略和算法,圖的遍歷都可理解爲,將非線性結構轉化爲半線性結構的過程。
經遍歷而確定的邊類型中,最重要的一類即所謂的樹邊,它們與所有頂點共同構成了原圖的一棵支撐樹(森林),稱作遍歷樹(traversal tree)。
以遍歷樹爲背景,其餘各種類型的邊,也能提供關於原圖的重要信息,比如其中所含的環路等。
圖中頂點之間可能存在多條通路,故爲避免對頂點的重複訪問,在遍歷的過程中,通常還要動態地設置各頂點不同的狀態,並隨着遍歷的進程不斷地轉換狀態,直至最後的“訪問完畢”。
圖的遍歷更加強調對處於特定狀態頂點的甄別與查找,故也稱作圖搜索(graph search)。
廣度優先搜索
各種圖搜索之間的區別,體現爲邊分類結果的不同,以及所得遍歷樹(森林)的結構差異。
其決定因素在於,搜索過程中的每一步迭代,將依照何種策略來選取下一接受訪問的頂點。
通常,都是選取某個已訪問到的頂點的鄰居。同一頂點所有鄰居之間的優先級,在多數遍歷中不必講究。因此,實質的差異應體現在,當有多個頂點已被訪問到,應該優先從誰的鄰居中選取下一頂點。
比如,廣度優先搜索(breadth-first search, BFS)採用的策略,可概括爲:
越早被訪問到的頂點,其鄰居越優先被選用
於是,始自圖中頂點的BFS搜索,將首先訪問頂點;再依次訪問所有尚未訪問到的鄰居;再按後者被訪問的先後次序,逐個訪問它們的鄰居;…;如此不斷。
在所有已訪問到的頂點中,仍有鄰居尚未訪問者,構成所謂的波峯集(frontier)(用隊列實現)。於是,BFS搜索過程也可等效地理解爲:
反覆從波峯集中找到最早被訪問到頂點v,若其鄰居均已訪問到,則將其逐出波峯集;否則,隨意選出一個尚未訪問到的鄰居,並將其加入到波峯集中。
不難發現,若將上述BFS策略應用於樹結構,則效果等同於層次遍歷。波峯集內頂點的深度始終相差不超過一,且波峯集總是優先在更淺的層次沿廣度方向拓展。
仿照樹的層次遍歷,這裏也藉助隊列,來保存已被發現,但尚未訪問完畢的頂點。
因此,任何頂點在進入該隊列的同時,都被隨即標記爲DISCOVERED(已發現)狀態。
BFS()的每一步迭代,都先從中取出當前的首頂點;再逐一覈對其各鄰居的狀態並做相應處理;最後將頂點置爲VISITED(訪問完畢)狀態,即可進入下一步迭代。
若頂點尚處於UNDISCOVERED(未發現)狀態,則令其轉爲DISCOVERED狀態,並隨即加入隊列。
實際上,每次發現一個這樣的頂點,都意味着遍歷樹可從到拓展一條邊。於是,將邊標記爲樹邊(tree edge),並按照遍歷樹中的承襲關係,將記作的父節點。
若頂點已處於DISCOVERED狀態(無向圖),或者甚至處於VISITED狀態(有向圖),則意味着邊不屬於遍歷樹,於是將該邊歸類爲跨邊(cross edge)。
BFS()遍歷結束後,所有訪問過的頂點通過parent[]指針依次聯接,從整體上給出了原圖某一連通或可達域的一棵遍歷樹,稱作廣度優先搜索樹,或簡稱BFS樹(BFS tree)。
實例:
不難看出,BFS(s)將覆蓋起始頂點所屬的連通分量或可達分量,但無法抵達此外的頂點。
而上層主函數bfs()的作用,正在於處理多個連通分量或可達分量並存的情況。具體地,在逐個檢查頂點的過程中,只要發現某一頂點尚未被發現,則意味着其所屬的連通分量或可達分量尚未觸及,故可從該頂點出發再次啓動BFS(),以遍歷其所屬的連通分量或可達分量。
如此,各次BFS()調用所得的BFS樹構成一個森林,稱作BFS森林(BFS forest)。
深度優先搜索
深度優先搜索(Depth-First Search, DFS)選取下一頂點的策略,可概括爲:
優先選取最後一個被訪問到的頂點的鄰居
於是,以頂點爲基點的DFS搜索,將首先訪問頂點;再從所有尚未訪問到的鄰居中任取其一,並以之爲基點,遞歸地執行DFS搜索。
故各頂點被訪問到的次序,類似於樹的先序遍歷;而各頂點被訪問完畢的次序,則類似於樹的後序遍歷。
每一遞歸實例中,都先將當前節點標記爲DISCOVERED(已發現)狀態,再逐一覈對其各鄰居的狀態並做相應處理。待其所有鄰居均已處理完畢之後,將頂點置爲VISITED(訪問完畢)狀態,便可回溯。
若頂點尚處於UNDISCOVERED(未發現)狀態,則將邊歸類爲樹邊(tree edge),並將記作的父節點。此後,便可將作爲當前頂點,繼續遞歸地遍歷。
若頂點處於DISCOVERED狀態,則意味着在此處發現一個有向環路。此時,在DFS遍歷樹中必爲的祖先,故應將邊歸類爲後向邊(back edge)。
這裏爲每個頂點都記錄了被發現的和訪問完成的時刻,對應的時間區間均稱作的活躍期(active duration)。實際上,任意頂點和之間是否存在祖先/後代的“血緣”關係,完全取決於二者的活躍期是否相互包含。
對於有向圖,頂點還可能處於VISITED狀態。此時,只要比對與的活躍期,即可判定在DFS樹中是否爲的祖先。若是,則邊應歸類爲前向邊(forward edge);否則,二者必然來自相互獨立的兩個分支,邊(v, u)應歸類爲跨邊(cross edge)。
DFS(s)返回後,所有訪問過的頂點通過parent[]指針依次聯接,從整體上給出了頂點所屬連通或可達分量的一棵遍歷樹,稱作深度優先搜索樹或DFS樹(DFS tree)。
與BFS搜索一樣,此時若還有其它的連通或可達分量,則可以其中任何頂點爲基點,再次啓動DFS搜索。最終,經各次DFS搜索生成的一系列DFS樹,構成了DFS森林(DFS forest)。
實例:
最終結果如上圖中所示,爲包含兩棵DFS樹的一個DFS森林。可以看出,選用不同的起始基點,生成的DFS樹(森林)也可能各異。如本例中,若從D開始搜索,則DFS森林可能如上圖中所示。
深度優先搜索的應用——拓撲排序
問題描述:
給定描述某一實際應用(上圖中)的有向圖(上圖中),如何在與該圖“相容”的前提下,將所有頂點排成一個線性序列(上圖中)。
此處的“相容”,準確的含義是:
每一頂點都不會通過邊,指向其在此序列中的前驅頂點。
這樣的一個線性序列,稱作原有向圖的一個拓撲排序。
同一有向圖的拓撲排序未必唯一,一個有向圖也未必一定存在拓撲排序。
有向無環圖一定存在拓撲排序。
思路一:
任一有向無環圖必包含入度爲零的頂點。否則,每個頂點都至少有一條入邊,意味着要麼頂點有無窮個,要麼包含環路。
於是,只要將入度爲0的頂點(及其關聯邊)從圖中取出,則剩餘的依然是有向無環圖,故其拓撲排序也必然存在。從遞歸的角度看,一旦得到了的拓撲排序,只需將作爲最大頂點插入,即可得到的拓撲排序。
思路二:
有向無環圖的DFS搜索過程中各頂點被標記爲VISITED的次序,恰好(按逆序)給出了原圖的一個拓撲排序。
相對於標準的DFS搜索算法,這裏增設了一個棧結構。一旦某個頂點被標記爲VISITED狀態,便隨即令其入棧。如此,當搜索終止時,所有頂點即按照被訪問完畢的次序——亦即拓撲排序的次序-——在棧中自頂而下排列。
最小支撐樹
連通圖的某一無環連通子圖若覆蓋中所有的頂點,則稱作的一棵支撐樹或生成樹(spanning tree)。
就保留原圖中邊的數目而言,支撐樹既是“禁止環路”前提下的極大子圖,也是“保持連通”前提下的最小子圖。在實際應用中,原圖往往對應於由一組可能相互聯接(邊)的成員(頂點)構成的系統,而支撐樹則對應於該系統最經濟的聯接方案。確切地,儘管同一幅圖可能有多棵支撐樹,但由於其中的頂點總數均爲n,故其採用的邊數也均爲n - 1。
若圖爲一帶權網絡,則每一棵支撐樹的成本(cost)即爲其所採用各邊權重的總和。在的所有支撐樹中,成本最低者稱作最小支撐樹(minimum spanning tree, MST)。
儘管同一帶權網絡通常都有多棵支撐樹,但總數畢竟有限,故必有最低的總體成本。然而,總體成本最低的支撐樹卻未必唯一。
Prim算法
假定各邊的權重互異。
圖中,頂點集的任一非平凡子集及其補集V\U都構成的一個割(cut),記作(U : V\U)。若邊滿足屬於,且不屬於,則稱作該割的一條跨越邊(crossing edge)。
因此類邊聯接於及其補集之間,故亦形象地稱作該割的一座橋(bridge)。
Prim算法的正確性基於以下事實:
最小支撐樹總是會採用聯接每一割的最短跨越邊。
否則,如上圖所示假設是割(U : V\U)的最短跨越邊,而最小支撐樹並未採用該邊。於是由樹的連通性,如圖所示在中必有至少另一跨邊聯接該割(有可能或,儘管二者不能同時成立)。同樣由樹的連通性,中必有分別聯接於和、和之間的兩條通路。由於樹是極大的無環圖,故倘若將邊加至中,則如圖所示,必然出現穿過和的唯一環路。接下來,只要再刪除邊,則該環路必然隨之消失。
經過如此的一出一入,若設轉換爲,則’依然是連通圖,且所含邊數與相同均爲。這就意味着,也是原圖的一棵支撐樹。就結構而言,與的差異僅在於邊和邊,故二者的成本之差就是這兩條邊的權重之差。不難看出,邊的權重必然大於身爲最短跨越邊的,故的總成本低於——這與總體權重最小的前提矛盾。
由以上性質,可基於貪心策略(當前看來是最好的選擇)導出一個迭代式算法。
實例:
最短路徑
若以帶權圖來表示真實的通訊、交通、物流或社交網絡,則各邊的權重可能代表信道成本、交通運輸費用或交往程度。此時我們經常關心的一類問題可以概括爲:
給定帶權網絡,以及源點(source)屬於,對於所有的其它頂點,到的最短通路有多長?該通路由哪些邊構成?
設頂點到的最短路徑爲。於是對於該路徑上的任一頂點,若其在上對應的前綴爲,則也必是到的最短路徑(之一)。否則,若從到還有另一嚴格更短的路徑,則易見不可能是到的最短路徑。
即便各邊權重互異,從到的最短路徑也未必唯一。
當存在非正權重的邊,並導致某個環路的總權值非正時,最短路徑甚至無從定義。
最短路徑不含任何(有向)迴路。
Dijkstra算法
該算法與Prim算法僅有一處差異:
考慮的是到的距離,而不再是其到的距離。
實例:
結語
如果您有修改意見或問題,歡迎留言或者通過郵箱和我聯繫。
手打很辛苦,如果我的文章對您有幫助,轉載請註明出處。