圖論演算法 有圖有程式碼 萬字總結 向前輩致敬
圖的定義
背景知識
看到這篇部落格相信一開始映入讀者眼簾的就是下面這幅圖了,這就是傳說中的七橋問題(哥尼斯堡橋問題)。在哥尼斯堡,普雷格爾河環繞著奈佛夫島(圖中的A島)。這條河將陸地分成了下面4個區域,該處還有著7座連線這些陸地的橋樑。
問題是如何從某地出發,依次沿著各個橋,必須經過每座橋且每座橋只能經過1次,最終回到原地。
不知道這個問題且好奇的童鞋現在肯定在忙活著找出來這道題的結果了。
是偉大的數學家尤拉(Leonhard Euler)在1736年首次使用圖的方法解決了該問題。
尤拉將上面的模型轉換成了下面這種”圖“的形式。
尤拉把頂點的度定義為與該頂點相關聯的邊的條數,並且他證明了存在從任意點出發,經過所有邊恰好一次,並最終回到出發頂點的走法的充分必要條件是:每個頂點的度均為偶數。人們稱之為尤拉閉跡(Eulerian walk)。
簡要定義
圖
有時也把邊稱作弧(arc),如果點對
頂點
我們可以給邊賦予各式的屬性,比如權值(cost)。權值可以表示從一個頂點到另一個頂點的距離,也可以表示一個頂點到另一個頂點說話費的代價(比如時間、金錢等)。一個邊上帶權值的圖稱為網路(network)。
如果無向圖中從每一個頂點到其他每個頂點都存在一條路徑,則稱該無向圖是連通的(connected)。具有這樣性質的有向圖稱為是強連通的的(strongly connected)。如果有向圖不是強連通的,但它的基礎圖(underlying graph)(也就是其弧上去掉方向說形成的圖)是連通的,那麼稱該有向圖是弱連通的(weakly connected)。完全圖(complete graph)是其每一對頂點間都存在一條邊的圖。
所謂入度(indegree)是指的頂點
如下表示了一個有著7個頂點和12條邊的有向圖。
如果具有n個頂點,e條邊的圖G的頂點i的度為
以上這個數學公式的markdown“原始碼”:
$ e =\frac { \sum_{0}^{n-1} d_i} {2} $
現在將圖看作抽象資料型別,下面給出ADT圖的結構:
objects | 一個非空頂點的集合和一個無向邊的集合,其中每條邊都是一個頂點對 |
---|
functions | 對於所有的 graph \in Graph , v , v_1 , v_2 \in Vertices |
---|---|
Graph Create() | return一個空圖 |
Graph InsertVertex (graph, v) | 向圖graph中插入沒有關聯邊的新頂點v,return改變後的圖 |
Graph InsertEdge (graph, v_1 , v_2 ) |
在圖graph的頂點 v_1 和v_2 之間插入一條邊,return改變後的圖 |
Graph DeleteVertex (graph, v) | 刪除圖graph的頂點v及與其關聯的所有邊,return改變後的圖 |
Graph DeleteEdge (graph, v_1 ,v_2 ) |
刪除圖graph的邊( v_1 ,v_2 ),頂點v_1 ,v_2 不刪除,return改變後的圖 |
Boolean IsEmpty (graph) | if(graph==空圖) return TRUE,else return FALSE |
List Adjacent (graph, v) | return頂點v的所有鄰接結點 |
圖的儲存表示方式
圖主要有3種常用的儲存表示方式:鄰接矩陣(adjacency matrices),鄰接表(adjacency lists),鄰接多重表(adjacency multilists)。
鄰接矩陣
鄰接矩陣使用
1)因為在無向圖中,我們只需要知道頂點
2)而在有向圖中,我們只需要知道是否有從頂點
3)在帶權值的圖中,
使用這種儲存方式,可以很方便地判斷任意兩個頂點之間是否有邊以及確定頂點的度,這也是這種表示法最大的優勢。任意一個頂點i的度等於其鄰接矩陣中頂點i所對應的行中的數字之和:
以上這個數學公式的markdown“原始碼”:
$ \sum_{j=0}^{n-1} g[i][j] $
在這種表示法中掃描所有邊至少需要
鄰接表
如果用鄰接矩陣表示稀疏圖就會浪費大量記憶體空間,而用連結表,則是通過把頂點所能到的頂點的邊儲存在連結串列中來表示圖,這樣就只需要
而所謂的鄰接表,就是用n個連結串列代替鄰接矩陣中的n行。連結串列中的結點結構至少要包含一個頂點域和一個鏈域。對於任意給定的連結串列i,連結串列中的結點就是與頂點i相鄰的所有頂點。鄰接表儲存宣告的C語言宣告如下:
#define MAX_VERTICES 50
typedef struct node *node-pointer;
typedef struct node
{
int vertex;
struct node *link;
};
node_pointer graph[MAX_VERTICES];
int n=0;
鄰接多重表
在無向圖的鄰接表儲存表示中,每一條邊
marked | vertex1 | vertex2 | path1 | path2 |
---|
鄰接多重表結點結構的C語言宣告為:
typedef struct edge *edge-pointer
typedef struct edge
{
short int marked;
int vertex1;
int vertex2;
edge_pointer path1;
edge_pointer path2;
};
圖的基本操作和演算法
廣度優先搜尋
請先忽視下圖中所有的下標,讓我們從頭開始。隨意選擇一個點,此處選擇
這就是廣度優先搜尋(breadth-first search),該方法按層處理頂點。距起始點最近的那些頂點首先被求值,最遠點則最後被求值,這很像對樹的層序遍歷(level-order traversal)。
為了實現廣度優先搜尋,可以使用動態連結佇列。在佇列中的每個頂點都包含兩個域:頂點的序號和連結指標。
函式bfs所使用的佇列的定義和函式原型宣告為:
typedef struct queue *queue_pointer;
typedef struct queue
{
int vertex;
queue_pointer link;
};
void addq(queue_pointer *, queue_pointer *,int);
int deleteq(queue_pointer *);
圖的廣度優先搜尋演算法:
void bfs(int v)
{
node_pointer w;
queue_pointer front,rear;
front=rear=NULL;
printf("%5d",v);
visited[v]=TRUE;
addq(&front,&rear,v);
while(front)
{
v=deleteq(&front);
for(w=graph[v];w;w=w->link)
{
if(!visited[w->vertex])
{
printf("%5d",w->vertex);
addq(&front,&rear,w->vertex);
visited[w->vertex]=TRUE;
}
}
}
}
圖中每個頂點都被存入佇列一次,所以該演算法中的while迴圈至多重複n次。如果採用鄰接表儲存表示,那麼該演算法所需要的時間為:
其中
而如果採用鄰接矩陣來實現,那麼對於每個頂點的訪問,while迴圈的時間為
深度優先搜尋
深度優先搜尋內容較多,已經在下文中單獨列出。
連通圖
使用以上的兩種搜尋演算法也可以用來判斷一個無向圖是否是連通的。具體步驟如下:
1.呼叫bfs(0)或dfs(0)
2.檢查是否存在未被訪問過的頂點
具體程式碼如下:
void connected(void)
{
int i;
for(i=0;i<n;i++)
{
if(!visited[i])
{
dfs(i);
printf("\n");
}
}
}
演算法分析:如果採用鄰接表儲存,那麼函式dfs時間開銷為
雙連通圖
雙聯通圖(biconnected graph)是沒有關節點的連通圖。對此有一個比較重要的公式如下:
low(u) = min{dfn(u), min{low(w)|w是u的兒子}, min{dfn(w)|(u,w)是一條回退邊} }
回退邊也叫back edge,大家顧名思義就好,下面有更多應用。
下面來段求解圖的雙連通分支的演算法:
void bicon(int u, int v)
{
node_pointer ptr;
int w,x,y;
dfn[u]=low[u]=num++;
for(ptr=graph[u];ptr;ptr=ptr->link)
{
w=ptr->vertex;
if(v!=w && dfn[w]<dfn[u])
add(&top,u,w);
if(dfn[w]<0)
{
bicon(w,u);
low[u]=MIN2(low[u],low[w]);
if(low[w]>=dfn[u])
{
printf("New biconnected component: ");
do
{
delete(&top,&x,&y);
printf(" <%d,%d>",x,y);
}while(!((x==u)&&(y==w)));
printf("\n");
}
}
else if(w!=v)
low[u]=MIN2(low[u],dfn[w]);
}
}
拓撲排序
拓撲排序(topological sort)是對有向無環圖的頂點的一種排序,它使得如果存在一條從vi到vj的路徑,那麼在排序中vj出現在vi的後面。正是由於這個特性,如果圖含有迴路,那麼拓撲排序是不可能的。
拓撲排序簡單的說,就是將上圖變成下圖。
求拓撲排序演算法的一種簡單方式:選中一個沒有入邊的頂點,顯示出該點,並將它和它的邊一起從圖中刪除,然後對圖的其餘部分應用同樣的方法處理。
假設每一個頂點的入度被儲存且圖被讀入一個鄰接表中,下面的程式碼則可以生成一個拓撲排序。
對上圖應用拓撲排序的結果如下:
最短路徑演算法
單源最短路徑問題:給定一個加權圖G=(V,E)和一個特定頂點s作為輸入,找出從s到G中每一個其他點的最短加權路徑。
如下圖所示,從v1到v6的最短加權路徑的值為6(
下面這個圖從v5到v4的最短加權路徑可就有意思了,它是1麼?不是。按照
當未指明所討論的是加權路徑還是無權路徑時,如果圖是加權的,那麼路徑就是加權的。
下面列出單源最短路徑演算法:
void shortestpath(int v,int cost[][MAX_VERTICES],int distance[],int n,short int found[])
{
int i,u,w;
for(i=0;i<n;i++)
{
found[i]=FALSE;
distance[i]=cost[v][i];
}
found[v]=TRUE;
distance[v]=0;
for(i=0;i<n-2;i++)
{
u=choose(distance,n,found);
found[u]=TRUE;
for(w=0;w<n;w++)
if(!found[w])
if(distance[u]+cost[u][w]<distance[w])
distance[w]=cost[u][w]+distance[u];
}
}
int choose(int distance[],int n,short int found[])
{
int i,min,minpos;
min=INT_MAX;
minpos=-1;
for(i=0;i<n;i++)
if(distance[i]<min && !found[i])
{
min=distance[i];
minpos=i;
}
return minpos;
}
思考:找出A到所有其他頂點的最短路徑以及B到所有其他頂點的最短無權路徑。
如果要求所有頂點對之間的最短路徑,可以用下面這個演算法:
void allcosts(int cost[][MAX_VERTICES],int distance[][MAX_VERTICES],int n)
{
int i,j,k;
for(i=0;i<n;i++)
for(j=0;j<n;j++)
distance[i][j]=cost[i][j];
for(k=0;k<n;k++)
for(i=0;i<n;i++)
for(j=0;j<n;j++)
if(distance[i][k]+distance[k][j]<distance[i][j])
distance[i][j]=distance[i][k]+distance[k][j];
}
傳遞閉包
我們由一個問題引入傳遞閉包的概念。有一個邊不帶權值的有向圖G,要判斷任意兩個頂點i 和j 之間是否存在一條路徑,此處有兩種情況,一種是路徑長度為正數,一種是路徑長度為非負。以上兩種情況分別被稱為圖的傳遞閉包(transitive closure),和自反傳遞閉包(reflexive transitive closure)。
傳遞閉包矩陣(transitive closure matrix)是一個矩陣,記作
自反傳遞閉包矩陣是一個矩陣,記作
Dijkstra演算法
前面的廣度優先搜尋中的圖是無權圖,而如果一旦變成了加權圖,那麼問題就變得困難起來了。
對於每個頂點,我們標記為known以及unknown,和上面一樣,還得有一個距離的
那麼這個演算法到底是怎麼回事了?請看下圖。
圖中已經對權重做好了標記,以
毫無疑問這裡會接下來走到v4去,因為v4的權重為1比v2的權重為2要小。調整為如下左圖。
可能你已經看到了上圖中的右圖而好奇為什麼下一步是
下一步便走到了
緊接著走到了
最後便順勢走到了v6完成了整個Dijkstra演算法,它們都已被標記為known。
在後面還將會有一種斐波那契堆,針對Dijkstra演算法做了優化,歡迎大家的繼續關注。
具有負邊值得圖
而如果一個圖具有負的邊值,那麼Dijkstra演算法就行不通了。這是因為一個頂點u被宣告為known後,那就可能從某個另外的unknown頂點v有一條回到u的負的路徑。而“回到”就意味著迴圈,前面的例子中我們已經知道了迴圈是多麼的……
問題並非沒有解決的辦法,如果我們有一個常數X,將其加到每一條邊的值上,這樣除去負的邊,再計算新圖的最短路徑,最後把結果應用到原圖上。然後這個解決方案也是佈滿了荊棘,因為居多許多條邊的路徑變得比那些具有很少邊的路徑權重更重了。如果我們將s放到佇列中,然後再每一個階段讓一個頂點v出隊,找出所有與v鄰接的頂點w,使得
無環圖
如果圖是無環的,則可以通過改變宣告頂點為known的順序,或者叫做頂點選取法則來改進Dijkstra演算法。這種方法通過拓撲排序來選擇頂點,由於選擇和更新可以在拓撲排序執行的過程中執行,因此新的演算法只需要一趟就可以完成。
通過下面這個動作結點圖(activity-node graph)來解釋什麼是關鍵路徑分析(critical path analysis)再合適不過了。一條邊
為了進行這些運算,我們把動作結點圖轉化成事件結點圖(event-node graph),每個事件對應於一個動作和所有與它相關的動作完成。
所以現在我們需要找出事件的最早完成時間,只要找出從第一個事件到最後一關事件的最長路徑的長。因為有正值迴路(positive-cost cycle)的存在最長路徑問題常常是沒有意義的。而由於事件結點圖是無環圖,那就不需要擔心迴路的問題了,這樣一來就不用有所顧忌了。
以下是最早完成時間。
以下是最晚完成時間。
藉助頂點的拓撲排序計算最早完成時間,而最晚完成時間則通過倒轉拓撲排序來計算。
而事件結點圖中每條邊的鬆弛時間(slack time)代表對應動作可以被延遲而不推遲整體完成的時間量,最早完成時間、最晚完成時間和鬆弛時間如下所示。
某些動作的鬆弛時間為0,這些動作是關鍵性的動作,它們必須按計劃結束。至少存在一條完成零-鬆弛邊組成的路徑,這樣的路徑是關鍵路徑(critical path)。
網路流問題
如下左圖所示,有一個頂點
要想計算最大流,同樣可是使用前面的思想——分階段進行。令開始時所有邊都沒有流,如下中間圖所示。我們可以用殘餘圖(residual graph)來表示對於每條邊還能再新增上多少流。對於每一條邊,可以從容量中減去當前的流而計算出殘留的流。
第一步:假設我們選擇
第二步:接下來選擇
第三步:從上圖的殘餘圖中我們已經可以看出來最後一步的唯一一種走法了,也就是從
很顯然從
如果一開始我們選擇了
為了使演算法得以成功運作,那麼就要讓流圖中具有以相反方向傳送流的路徑,如下所示。那麼對於如下右圖中的殘餘圖而言,從d返回到a的便成了3而非4,這是因為從t流到d的流量是3個單位。現在在殘餘圖中就有a和d之間有2個方向,或者還有1個單位的流可以從
緊接著如果通過
思考:找出下面網路中的一個拓撲排序以及最大流。
活動網路
AOV網路
除了一些不能再簡單的工程外,所有的工程都可以劃分為若干個成為活動(activities)的子工程。比如說大學的課程,得修完了大學英語1才能修大學英語2,也得修完了高等數學才能修線性代數、概率論、離散數學等。將這些作為頂點標記為圖時,它便是一個有向圖。
頂點表示活動的網(activity on vertex network)或AOV網,是用頂點表示活動或任務,邊表示活動或任務之間的優先關係的有向圖G。在AOV網路G中,當且僅當從頂點i到j存在一條有向路徑,則頂點i稱為頂點j的前驅(predecessor);當且僅當
拓撲排列是由有向圖中所有頂點形成一個線性序列,對圖中任意兩個頂點
我們在前面已經介紹了拓撲排序,這裡給出它的虛擬碼。
for(i=0;i<n;i++)
{
if every vertex has a predecessor
{
fprintf(stderr,"Network has a cycle.\n");
exit(1);
}
pick a vertex v that has no predecessors;
output v;
delete v and all edges leading out of v from the netwok;
}
對於拓撲排序問題,所需的操作主要有:
1)判斷頂點是否有前驅;
2)刪除頂點和關聯於該頂點的所有邊。
在操作1中,我們可以在每個頂點中都儲存其直接前驅個數的計數。對於操作2,可以使用前面介紹過的鄰接表來表示AOV網路。於是可以將其實現為以下C程式碼:
// 宣告
typedef struct node *node_pointer;
typedef struct node
{
int vertex;
node_pointer link;
};
typedef struct
{
int count;
node_pointer link;
}hdnodes;
hdnodes graph[MAX_VERTICES];
// 函式
void topsort(hdnodes graph[],int n)
{
int i,j,k,top;
node_pointer ptr;
top=-1;
for(i=0;i<n;i++)
{
if(!graph[i].count)
{
graph[i].count=top;
top=i;
}
}
for(i=0;i<n;i++)
{
if(top==-1)
{
fprintf(stderr,"\nNetwork has a cycle. Sort terminated.\n");
exit(1);
}
else
{
j=top;
top=graph[top].count;
printf("v%d, ",j);
for(ptr=graph[j].link;ptr;ptr=ptr->link)
{
k=ptr->vertex;
graph[k].count--;
if(!graph[k].count)
{
graph[k].count=top;
top=k;
}
}
}
}
}
在topsort的宣告中,count域用來儲存頂點的入度,而link域則是指向鄰接表首結點的指標。鄰接表中的每個結點又包含了兩個域:vertex和link。在輸入時,可以方便地設定count域的值。當輸入一條邊
對於topsort的分析:對於具有n個頂點和e條邊的AOV網路,第一個for迴圈的時間開銷為
因此這個演算法的漸進時間為O(e+n),與問題的規模呈線性關係。
AOE網路
AOE網路就是邊表示活動的網路(activity on edge network),它的有向邊表示在一個工程中所需完成的任務或活動,而頂點表示事件,用來標識某些活動的完成。在AOV網路中,事件高數2完成意味著要先完成高數1;AOE網路中,事件高數2完成意味著已經完成了高數1。也就是說在AOE中,當一個事件發生時,就表明觸發該事件的所有活動都已經完成。
在AOE網路中,有些活動可以並行地進行,所以完成整個工程所需的最短時間是從開始頂點到終止頂點的最長路徑的長度。關鍵路徑(critical path)就是一條具有最長路徑長度的路徑。
一個事件
關鍵網路(critical activity)是指滿足
下面列出兩個比較常用的公式:
1)事件最早發生時間的計算
以上這個數學公式的markdown“原始碼”:
$ earliest[j] = \displaystyle \max_{x \in {P(j)}} \{ earliest[i] + <i , j> 的持續時間 \} $
2)事件最晚發生時間的計算
最小生成樹
一個無向圖G的最小生成樹(minimum spanning tree)就是由該圖的那些連線G的所有頂點的邊構成的總值最低的樹。最小生成樹存在當且僅當G是連通的。
下面第二個圖是第一個圖的最小生成樹(碰巧是唯一的,但並不能代表一般情況)。最小生成樹是一棵樹;因為它無環;因為最小生成樹包含每一個頂點,所以它叫生成樹;此外,它顯然是包含所有頂點的最小的樹。
Prim演算法
計算最小生成樹的一種方法是使其連續地一步一步成長,在每一步中,都要把一個結點當作根並且往上累加邊,於是就將關聯的頂點加到了增長中的樹上。
Prim演算法和前面求最短路徑的Dijkstra演算法思想類似,因此和前面一樣我們對每一個頂點保留值dv和pv以及一個標記頂點的known或unknown。
還是老辦法,在Prim演算法中也設定一個表的初始狀態如下。
將v1設定為known的,根據Prim演算法上一張圖所示,v1連線了v2、v3、v4,其dv分別為2、4、1,因此更新如下。
將v4宣告為known,更新如下。
將v2和v3先後宣告為known,更新如下。
將v7宣告為known後更新如下左圖,最後將v6和v5也更新為known後更新如下右圖。
下面是Prim演算法的虛擬碼實現,其中T為生成樹的邊集,TV是當前生成樹T的頂點集合
Kruskal演算法
第二種貪心策略是連續地按照最小的權選擇邊,並且當所選的邊不產生迴路時就把它作為取定的邊。同樣是前面的示例,用Kruskal演算法執行如下。
形式上,Kruskal演算法是在處理一個森林——樹的集合。下圖展示了邊被新增到森林中的順序。
當新增到森林中的邊足夠多時,演算法終止,這裡演算法的作用在於決定邊
在該演算法執行的過程中,兩個頂點屬於同一個集合當且僅當它們在當前的生成森林(spanning forest)中連通。如果
如果這兩個頂點不在同一個集合中,就應該將這條邊加入,並對包含頂點
Sollin演算法
Sollin演算法在每一步都為生成樹遞迴地選取若干條邊,在每一步處理開始時,說選取的邊與圖中的所有n個頂點形成一個生成森林。在執行過程中,為森林中的每棵樹都選取一條邊,與選取的邊都是恰有一個頂點在樹上且代價最小。由於森林中的兩棵樹可能選取同一條邊,所以需要去掉同一條邊被多次選取的情況。在開始時,說選取的邊集為空,當最後結果只有一棵樹或者再沒有邊可供選取時,演算法就此結束。
深度優先搜尋
深度優先搜尋(depth-first search)是對前序遍歷的推廣,對每一個頂點,欄位visited被初始化成false,通過哪些尚未被訪問的結點遞迴呼叫該過程,我們保證不會陷入無限迴圈。如果圖是無向且連通的,或是有向但非強連通的,這種方法可能會訪問不到某些結點。此時我們搜尋一個未被標記的結點,然後應用深度優先遍歷,並繼續這個過程直到不存在未標記的結點為止。因為該方法保證每一條邊只被訪問一次,所以只要使用鄰接表,執行遍歷的總時間就是
深度優先搜尋的演算法實現:
#define FALSE 0
#define TRUE 1
short int visited[MAX_VERTICES]
void dfs(int v)
{
node_pointer w;
visited[v]=TRUE;
printf("%5d",v);
for(w=graph[v];w;w=w->link);
if(!visited[w->vertex])
dfs(w->vertex);
}
和上文中的廣搜一樣,我們也來對dfs進行分析。
如果採用鄰接表來儲存,就可以沿著頂點的連結串列來確定其所有鄰接頂點。因此在鄰接表中的每一個頂點都至多掃描一次,所以完成搜尋時間複雜性為
如果採用鄰接矩陣來儲存,訪問頂點的所有鄰接頂點的時間為
無向圖
如下左圖中是一個無向圖,我們以此為例,假設從A開始,標記A為known並遞迴地呼叫dfs(B)。dfs(B)標記B為known並遞迴呼叫dfs(C)。dfs(C)標記C為known並遞迴呼叫dsf(D)。而後D的鄰接結點只有C,但C已經是knwon的,這裡便無法再繼續遞迴下去,因此返回到dfs(C)。dfs(C)看到已經標記為known的B鄰接點和D鄰接點,因此呼叫dfs(E)。dfs(E)標記E為known,同樣的它只能返回到dfs(C),再返回到dfs(B),最後返回到dfs(A)。實際上這裡接觸每條邊2次,一次是作為邊(v,w),另一次是作為邊(w,v)。
如下右圖展示了深度優先搜尋樹(depth-first spanning tree)的步驟。虛線表示向後邊(back edge),表示這條“邊”並不是樹的一部分。
樹將模擬我們執行的遍歷,使用樹的邊對該樹的前序編號(preorder numbering)表示頂點被標記的順序;如果圖不是連通的,那麼處理所有的結點(以及邊)自然需要多次反覆呼叫dfs,每次都生成一棵樹,整個集合就是深度優先生成森林(depth-first spanning forest)。
雙連通性
前面我們已經介紹過了雙連通圖,如果刪除一個無向圖的仁一頂點後,剩下的圖仍然連通,那麼這樣的無向連通圖就稱為是雙連通的(biconnected)。
如果圖不是雙聯通的,那麼將其刪除後不再連通的那些頂點叫做割點(articulation point)。
深度優先搜尋提供了一種找出連通圖中所有割點的線性時間演算法。從圖中任一頂點開始,執行深度優先搜尋並在頂點被訪問時給它們編號。對於每一個頂點v,我們稱其前序編號為Num(V)。然後,對於深度優先搜尋生成樹中的每一個頂點v,計算編號最低的頂點,我們稱之為Low(V),該點可從v開始通過樹的零條或多條邊,且可能還有一條後向邊而以該序達到。
有向圖
如前所述,如果圖不是強連通的,那麼從某個結點開始的深度優先搜尋可能訪問不了所有的結點,這種情況下我們從某個未做標記的結點處開始,反覆執行深度優先搜尋,直到所有的結點都被訪問到為止。
對此我們從頂點
深度優先生成森林中的虛線是一些
深度優先搜尋還可以用來檢測一個有向圖是否是無環圖,因為一個有向圖是無環圖當且僅當它沒有向後邊。上面的例子明顯不是無環圖,因為它有向後邊。而拓撲排序也可以用來檢測一個圖是否是無環圖,進行拓撲排序的另一種方法是通過深度優先搜尋生成森林的後序遍歷給頂點指定拓撲編號
備註1:為保證權威性,所有定義、公式、圖片均來自《資料結構(C語言版)》、《資料結構與演算法分析 C++描述》、《離散數學》、《演算法導論》、《挑戰程式設計競賽》等書籍和必應、維基百科等網路。
備註2:希望不會因為參考了在備註1中提到的諸多書籍與網路而被批判,畢竟這篇部落格也融合了我許多時間、精力和思考。我只是希望像我所讀的所有外國書籍一般在書尾列出參考文獻一樣尊重原作者。
備註3:個人能力有限,暫時無力為圖論這一領域增加新演算法,本文中所有演算法均來自偉大的前輩們的著作,為表敬意,此處以金色顯示。
感謝您的訪問,希望對您有所幫助。
歡迎大家關注或收藏、評論或點贊。
為使本文得到斧正和提問,轉載請註明出處:
http://blog.csdn.net/nomasp
相關文章
- 圖論總結圖論
- 圖論(三)--各種基礎圖演算法總結圖論演算法
- 圖論-有向圖縮點圖論
- 圖論 最短路總結圖論
- 圖的演算法的總結演算法
- 圖 - 有向圖
- 圖論演算法圖論演算法
- 圖論與圖學習(二):圖演算法圖論演算法
- 【圖論】Floyd演算法圖論演算法
- 圖論系列之「讀取圖演算法」圖論演算法
- 廣度優先搜尋相關面試演算法總結(非圖論方面)面試演算法圖論
- 有向圖的基本演算法-Java實現演算法Java
- 圖論-二分圖匹配匈牙利演算法圖論演算法
- 程式碼隨想錄day 53 || 圖論4圖論
- 程式碼隨想錄演算法訓練營第64天 | 圖論:Floyd 演算法+A * 演算法演算法圖論
- RecyclerView快取原理,有圖有真相View快取
- 微信小程式地圖開發總結微信小程式地圖
- 圖解Dijkstra演算法+程式碼實現圖解演算法
- 圖知識總結
- 演算法資料結構 | 圖論基礎演算法——拓撲排序演算法資料結構圖論排序
- 「圖論」Bron-kerbosch演算法圖論演算法
- 演算法-圖論-拓撲排序演算法圖論排序
- 總有那麼一張圖會讓你噴飯
- 程式碼隨想錄演算法訓練營第52天 | 圖論基礎1演算法圖論
- 演算法流程圖怎麼畫,免費模板有哪些演算法流程圖
- 【JAVA演算法】圖論演算法 -- Dijkstra演算法Java演算法圖論
- 微信小程式 canvas 繪圖問題總結微信小程式Canvas繪圖
- 因子圖相關理論彙總
- 圖論最短路演算法筆記圖論演算法筆記
- 演算法-圖論-最小生成樹演算法圖論
- 圖論演算法遍歷基礎圖論演算法
- 圖片隱寫總結
- 程式碼隨想錄演算法訓練營 | 圖論理論基礎,98. 所有可達路徑演算法圖論
- 判斷一個有向圖是否有環
- 資料結構 - 圖之程式碼實現資料結構
- 圖解HTTP知識總結(思維導圖)圖解HTTP
- MPAndroidChart繪製曲線圖、柱狀圖總結Android
- 圖的鄰接表演算法---(附完整程式碼)演算法
- 如何製作地圖?有沒有什麼超好用的地圖軟體?地圖