【程序人生】數據結構雜記(六)

說在前面

個人讀書筆記

圖的概念

圖結構是描述和解決實際應用問題的一種基本而有力的工具。

所謂的圖(graph),可定義爲G=(V,E)G = (V, E)。其中,集合VV中的元素稱作頂點(vertex);集合EE中的元素分別對應於VV中的某一對頂點(u,v)(u, v)表示它們之間存在某種關係,故亦稱作邊(edge)

一種直觀顯示圖結構的方法是,用小圓圈或小方塊代表頂點,用聯接於其間的直線段或曲線弧表示對應的邊。

從計算的需求出發,我們約定VVEE均爲有限集。

無向圖、有向圖及混合圖

若邊(u,v)(u, v)所對應頂點uuvv的次序無所謂,則稱作無向邊(undirected edge),例如表示同學關係的邊。
反之若uuvv不對等,則稱(u,v)(u, v)爲有向邊(directed edge),例如描述企業與銀行之間的借貸關係,或者程序之間的相互調用關係的邊。
如此,無向邊(u,v)(u, v)也可記作(v,u)(v, u),而有向的(u,v)(u, v)(v,u)(v, u)則不可混淆。這裏約定,有向邊(u,v)(u, v)uu指向vv,其中uu稱作該邊的起點(origin)或尾頂點(tail),而vv稱作該邊的終點(destination)或頭頂點(head)

EE中各邊均無方向,則GG稱作無向圖。若EE中只含有向邊,則GG稱作有向圖。若EE同時包含無向邊和有向邊,則GG稱作混合圖。

相對而言,有向圖的通用性更強,因爲無向圖和混合圖都可轉化爲有向圖:
每條無向邊(u,v)(u, v)都可等效地替換爲對稱的一對有向邊(u,v)(u, v)(v,u)(v, u)

對於任何邊e=(u,v)e = (u, v)稱頂點uuvv彼此鄰接(adjacent),互爲鄰居;而它們都與邊ee彼此關聯(incident)

在這裏插入圖片描述
在無向圖中,與頂點vv關聯的邊數,稱作vv的度數(degree),記作deg(v)deg(v)
以上圖中無向圖爲例,頂點{ A, B, C, D }的度數爲{ 2, 3, 2, 1 }。

對於有向邊e=(u,v)e = (u, v)ee稱作uu的出邊(outgoing edge)、vv的入邊(incoming edge)vv的出邊總數稱作其出度(out-degree),記作outdeg(v)outdeg(v);入邊總數稱作其入度(in-degree),記作indeg(v)indeg(v)
在上圖中的有向圖中,各頂點的出度爲{ 1, 3, 1, 1 },入度爲{ 2, 1, 2, 1 }。

簡單圖

聯接於同一頂點之間的邊,稱作自環(self-loop)。在某些特定的應用中,這類邊可能的確具有意義——比如在城市交通圖中,沿着某條街道,有可能不需經過任何交叉路口即可直接返回原處。
不含任何自環的圖稱作簡單圖(simple graph)

通路和環路

所謂路徑或通路(path),就是由m+1m + 1個頂點與mm條邊交替而成的一個序列,且對任何0<i<=m0 < i <= m都有ei=(vi1,vi)e_i = (v_{i-1} , v_i )。也就是說,這些邊依次地首尾相聯其中沿途邊的總數mm,亦稱作通路的長度

儘管通路上的邊必須互異,但頂點卻可能重複。沿途頂點互異的通路,稱作簡單通路

特別地,對於長度m>=1m >= 1的通路,若起止頂點相同(即v0=vmv_0 = v_m),則稱作環路(cycle),其長度也取作沿途邊的總數。同樣,儘管環路上的各邊必須互異,但頂點卻也可能重複。反之,若沿途除v0=vmv_0 = v_m外所有頂點均互異,則稱作簡單環路。

特別地,經過圖中各邊一次且恰好一次的環路,稱作歐拉環路(Eulerian tour)——其長度恰好等於圖中邊的總數。經過圖中各頂點一次且恰好一次的環路,稱作哈密爾頓環路(Hamiltonian tour)。

不含任何環路的有向圖,稱作有向無環圖

帶權網絡

圖不僅需要表示頂點之間是否存在某種關係,有時還需要表示這一關係的具體細節。以鐵路運輸爲例,可以用頂點表示城市,用頂點之間的聯邊表示對應的城市之間是否有客運鐵路聯接;同時,往往還需要記錄各段鐵路的長度、承運能力,以及運輸成本等信息。

爲適應這類應用要求,需通過一個權值函數,爲每一邊ee指定一個權重(weight),比如wt(e)wt(e)即爲邊ee的權重。各邊均帶有權重的圖,稱作帶權圖(weighted graph)或帶權網絡(weightednetwork),有時也簡稱網絡(network),記作G(V,E,wt())G(V, E, wt())

圖ADT的實現方法

鄰接矩陣

鄰接矩陣(adjacency matrix)是圖ADT(Abstract Data Type)最基本的實現方式,使用方陣A[n][n]A[n][n]表示由nn個頂點構成的圖,其中每個單元,各自負責描述一對頂點之間可能存在的鄰接關係,故此得名。
在這裏插入圖片描述
對於無權圖,存在(不存在)從頂點uuvv的邊,當且僅當A[u][v]=1(0)A[u][v] =1(0)。上圖中(a)(a)(b)(b)即爲無向圖和混合圖的鄰接矩陣實例。

對於帶權網絡,如上圖中(c)(c)所示,矩陣各單元可從布爾型改爲整型或浮點型,記錄所對應邊的權重。對於不存在的邊,通常統一取值爲無窮大或0。

鄰接表

在這裏插入圖片描述
以如上圖中(a)(a)所示的無向圖爲例,只需將如上圖中(b)(b)所示的鄰接矩陣,逐行地轉換爲如上圖中(c)(c)所示的一組列表,即可分別記錄各頂點的關聯邊(或等價地,鄰接頂點)。這些列表,也因此稱作鄰接表(adjacency list)。

圖遍歷算法

無論採用何種策略和算法,圖的遍歷都可理解爲,將非線性結構轉化爲半線性結構的過程

經遍歷而確定的邊類型中,最重要的一類即所謂的樹邊,它們與所有頂點共同構成了原圖的一棵支撐樹(森林),稱作遍歷樹(traversal tree)。

以遍歷樹爲背景,其餘各種類型的邊,也能提供關於原圖的重要信息,比如其中所含的環路等。

圖中頂點之間可能存在多條通路,故爲避免對頂點的重複訪問,在遍歷的過程中,通常還要動態地設置各頂點不同的狀態,並隨着遍歷的進程不斷地轉換狀態,直至最後的“訪問完畢”。

圖的遍歷更加強調對處於特定狀態頂點的甄別與查找,故也稱作圖搜索(graph search)。

廣度優先搜索

各種圖搜索之間的區別,體現爲邊分類結果的不同,以及所得遍歷樹(森林)的結構差異。
其決定因素在於,搜索過程中的每一步迭代,將依照何種策略來選取下一接受訪問的頂點。

通常,都是選取某個已訪問到的頂點的鄰居。同一頂點所有鄰居之間的優先級,在多數遍歷中不必講究。因此,實質的差異應體現在,當有多個頂點已被訪問到,應該優先從誰的鄰居中選取下一頂點

比如,廣度優先搜索(breadth-first search, BFS)採用的策略,可概括爲:
越早被訪問到的頂點,其鄰居越優先被選用

於是,始自圖中頂點ss的BFS搜索,將首先訪問頂點ss;再依次訪問ss所有尚未訪問到的鄰居;再按後者被訪問的先後次序,逐個訪問它們的鄰居;…;如此不斷。

在所有已訪問到的頂點中,仍有鄰居尚未訪問者,構成所謂的波峯集(frontier)(用隊列實現)。於是,BFS搜索過程也可等效地理解爲:
反覆從波峯集中找到最早被訪問到頂點v,若其鄰居均已訪問到,則將其逐出波峯集;否則,隨意選出一個尚未訪問到的鄰居,並將其加入到波峯集中。

不難發現,若將上述BFS策略應用於樹結構,則效果等同於層次遍歷。波峯集內頂點的深度始終相差不超過一,且波峯集總是優先在更淺的層次沿廣度方向拓展。

在這裏插入圖片描述
仿照樹的層次遍歷,這裏也藉助隊列QQ,來保存已被發現,但尚未訪問完畢的頂點

因此,任何頂點在進入該隊列的同時,都被隨即標記爲DISCOVERED(已發現)狀態。

BFS()的每一步迭代,都先從QQ中取出當前的首頂點vv;再逐一覈對其各鄰居uu的狀態並做相應處理;最後將頂點vv置爲VISITED(訪問完畢)狀態,即可進入下一步迭代。

若頂點uu尚處於UNDISCOVERED(未發現)狀態,則令其轉爲DISCOVERED狀態,並隨即加入隊列QQ

實際上,每次發現一個這樣的頂點uu都意味着遍歷樹可從vvuu拓展一條邊。於是,將邊(v,u)(v, u)標記爲樹邊(tree edge),並按照遍歷樹中的承襲關係,將vv記作uu的父節點。

若頂點uu已處於DISCOVERED狀態(無向圖),或者甚至處於VISITED狀態(有向圖),則意味着邊(v,u)(v, u)不屬於遍歷樹,於是將該邊歸類爲跨邊(cross edge)。

BFS()遍歷結束後,所有訪問過的頂點通過parent[]指針依次聯接,從整體上給出了原圖某一連通或可達域的一棵遍歷樹,稱作廣度優先搜索樹,或簡稱BFS樹(BFS tree)。

實例:
在這裏插入圖片描述不難看出,BFS(s)將覆蓋起始頂點ss所屬的連通分量或可達分量,但無法抵達此外的頂點

而上層主函數bfs()的作用,正在於處理多個連通分量或可達分量並存的情況。具體地,在逐個檢查頂點的過程中,只要發現某一頂點尚未被發現,則意味着其所屬的連通分量或可達分量尚未觸及,故可從該頂點出發再次啓動BFS(),以遍歷其所屬的連通分量或可達分量。

如此,各次BFS()調用所得的BFS樹構成一個森林,稱作BFS森林(BFS forest)。

深度優先搜索

深度優先搜索(Depth-First Search, DFS)選取下一頂點的策略,可概括爲:
優先選取最後一個被訪問到的頂點的鄰居

於是,以頂點ss爲基點的DFS搜索,將首先訪問頂點ss;再從ss所有尚未訪問到的鄰居中任取其一,並以之爲基點,遞歸地執行DFS搜索。

故各頂點被訪問到的次序,類似於樹的先序遍歷;而各頂點被訪問完畢的次序,則類似於樹的後序遍歷。

在這裏插入圖片描述
每一遞歸實例中,都先將當前節點vv標記爲DISCOVERED(已發現)狀態,再逐一覈對其各鄰居uu的狀態並做相應處理。待其所有鄰居均已處理完畢之後,將頂點vv置爲VISITED(訪問完畢)狀態,便可回溯。

若頂點uu尚處於UNDISCOVERED(未發現)狀態,則將邊(v,u)(v, u)歸類爲樹邊(tree edge),並將vv記作uu的父節點。此後,便可將uu作爲當前頂點,繼續遞歸地遍歷。

若頂點uu處於DISCOVERED狀態,則意味着在此處發現一個有向環路。此時,在DFS遍歷樹中uu必爲vv的祖先,故應將邊(v,u)(v, u)歸類爲後向邊(back edge)。

這裏爲每個頂點vv都記錄了被發現的和訪問完成的時刻,對應的時間區間[dTime(v),fTime(v)][dTime(v),fTime(v)]均稱作vv的活躍期(active duration)。實際上,任意頂點vvuu之間是否存在祖先/後代的“血緣”關係,完全取決於二者的活躍期是否相互包含。

對於有向圖,頂點uu還可能處於VISITED狀態。此時,只要比對vvuu的活躍期,即可判定在DFS樹中vv是否爲uu的祖先。若是,則邊(v,u)(v, u)應歸類爲前向邊(forward edge);否則,二者必然來自相互獨立的兩個分支,邊(v, u)應歸類爲跨邊(cross edge)。

DFS(s)返回後,所有訪問過的頂點通過parent[]指針依次聯接,從整體上給出了頂點ss所屬連通或可達分量的一棵遍歷樹,稱作深度優先搜索樹或DFS樹(DFS tree)。

與BFS搜索一樣,此時若還有其它的連通或可達分量,則可以其中任何頂點爲基點,再次啓動DFS搜索。最終,經各次DFS搜索生成的一系列DFS樹,構成了DFS森林(DFS forest)。

實例:
在這裏插入圖片描述
在這裏插入圖片描述
最終結果如上圖中(t)(t)所示,爲包含兩棵DFS樹的一個DFS森林。可以看出,選用不同的起始基點,生成的DFS樹(森林)也可能各異。如本例中,若從D開始搜索,則DFS森林可能如上圖中(u)(u)所示。

深度優先搜索的應用——拓撲排序

問題描述:
在這裏插入圖片描述

給定描述某一實際應用(上圖中(a)(a))的有向圖(上圖中(b)(b)),如何在與該圖“相容”的前提下,將所有頂點排成一個線性序列(上圖中(c)(c))。

此處的“相容”,準確的含義是:
每一頂點都不會通過邊,指向其在此序列中的前驅頂點。

這樣的一個線性序列,稱作原有向圖的一個拓撲排序。
同一有向圖的拓撲排序未必唯一,一個有向圖也未必一定存在拓撲排序。
有向無環圖一定存在拓撲排序。

思路一:
任一有向無環圖必包含入度爲零的頂點。否則,每個頂點都至少有一條入邊,意味着要麼頂點有無窮個,要麼包含環路。
於是,只要將入度爲0的頂點mm(及其關聯邊)從圖GG中取出,則剩餘的GG'依然是有向無環圖,故其拓撲排序也必然存在。從遞歸的角度看,一旦得到了GG'的拓撲排序,只需將mm作爲最大頂點插入,即可得到GG的拓撲排序。
在這裏插入圖片描述

思路二:
有向無環圖的DFS搜索過程中各頂點被標記爲VISITED的次序,恰好(按逆序)給出了原圖的一個拓撲排序。
在這裏插入圖片描述
在這裏插入圖片描述

相對於標準的DFS搜索算法,這裏增設了一個棧結構。一旦某個頂點被標記爲VISITED狀態,便隨即令其入棧。如此,當搜索終止時,所有頂點即按照被訪問完畢的次序——亦即拓撲排序的次序-——在棧中自頂而下排列

最小支撐樹

連通圖GG的某一無環連通子圖TT若覆蓋GG中所有的頂點,則稱作GG的一棵支撐樹或生成樹(spanning tree)。
在這裏插入圖片描述就保留原圖中邊的數目而言,支撐樹既是“禁止環路”前提下的極大子圖,也是“保持連通”前提下的最小子圖。在實際應用中,原圖往往對應於由一組可能相互聯接(邊)的成員(頂點)構成的系統,而支撐樹則對應於該系統最經濟的聯接方案。確切地,儘管同一幅圖可能有多棵支撐樹,但由於其中的頂點總數均爲n,故其採用的邊數也均爲n - 1

若圖GG爲一帶權網絡,則每一棵支撐樹的成本(cost)即爲其所採用各邊權重的總和。在GG的所有支撐樹中,成本最低者稱作最小支撐樹(minimum spanning tree, MST)。

儘管同一帶權網絡通常都有多棵支撐樹,但總數畢竟有限,故必有最低的總體成本。然而,總體成本最低的支撐樹卻未必唯一

Prim算法

假定各邊的權重互異

G=(V;E)G = (V; E)中,頂點集VV的任一非平凡子集UU及其補集V\U都構成GG的一個割(cut),記作(U : V\U)。若邊uvuv滿足uu屬於UU,且vv不屬於UU,則稱作該割的一條跨越邊(crossing edge)。

因此類邊聯接於VV及其補集之間,故亦形象地稱作該割的一座橋(bridge)。

Prim算法的正確性基於以下事實:
最小支撐樹總是會採用聯接每一割的最短跨越邊。

在這裏插入圖片描述
否則,如上圖(a)(a)所示假設uvuv是割(U : V\U)的最短跨越邊,而最小支撐樹TT並未採用該邊。於是由樹的連通性,如圖(b)(b)所示在TT中必有至少另一跨邊stst聯接該割(有可能s=us = ut=vt =v,儘管二者不能同時成立)。同樣由樹的連通性,TT中必有分別聯接於uussvvtt之間的兩條通路。由於樹是極大的無環圖,故倘若將邊uvuv加至TT中,則如圖(c)(c)所示,必然出現穿過uvtu、v、tss的唯一環路。接下來,只要再刪除邊stst,則該環路必然隨之消失。

經過如此的一出一入,若設TT轉換爲TT',則TT’依然是連通圖,且所含邊數與TT相同均爲n1n - 1。這就意味着,TT'也是原圖的一棵支撐樹。就結構而言,TT'TT的差異僅在於邊uvuv和邊stst,故二者的成本之差就是這兩條邊的權重之差。不難看出,邊stst的權重必然大於身爲最短跨越邊的uvuv,故TT'的總成本低於TT——這與TT總體權重最小的前提矛盾。

由以上性質,可基於貪心策略(當前看來是最好的選擇)導出一個迭代式算法。

實例:
在這裏插入圖片描述在這裏插入圖片描述

最短路徑

若以帶權圖來表示真實的通訊、交通、物流或社交網絡,則各邊的權重可能代表信道成本、交通運輸費用或交往程度。此時我們經常關心的一類問題可以概括爲:
給定帶權網絡G=(V,E)G = (V, E),以及源點(source)ss屬於VV,對於所有的其它頂點vvssvv的最短通路有多長?該通路由哪些邊構成?
在這裏插入圖片描述

設頂點ssvv的最短路徑爲pp。於是對於該路徑上的任一頂點uu,若其在pp上對應的前綴爲σ\sigma,則σ\sigma也必是ssuu的最短路徑(之一)。否則,若從ssuu還有另一嚴格更短的路徑τ\tau,則易見pp不可能是ssvv的最短路徑。

即便各邊權重互異,從ssvv的最短路徑也未必唯一。
當存在非正權重的邊,並導致某個環路的總權值非正時,最短路徑甚至無從定義。

最短路徑不含任何(有向)迴路。

Dijkstra算法

該算法與Prim算法僅有一處差異:
考慮的是uk+1u_{k+1}ss的距離,而不再是其到TkT_k的距離。

實例:
在這裏插入圖片描述

結語

如果您有修改意見或問題,歡迎留言或者通過郵箱和我聯繫。
手打很辛苦,如果我的文章對您有幫助,轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章