資料結構和演算法:第八章 圖論演算法
9.1 若干定義
-
圖的定義:一個圖(Graph) G=(V,E)是由頂點的集合V和邊Edge的集合E組成的。每一條邊就是一個頂點對(v,w),其中(v,w) ∈E。有時候也把邊叫做弧。如果頂點對是有序的,那麼圖就是有向的。有的圖也叫做有向圖。頂點w和頂點v鄰接當且僅當(v,w)∈E。在一個具有邊(v,w)從而具有邊(w,v)的無向圖中,w和v鄰接且v和w鄰接。有時候邊還有第三種成分,成為權值。
-
圖的路徑:圖中的一條路徑是指一個頂點序列w1,w2,w3,w4…,wn使得(wi,wi+1)∈E,這樣一條路徑的長就是改路徑的邊的數量,它等於N-1。從一個頂點到它自身可以看做一個路徑,如果路徑不包含邊,則該路徑的長為0。一條簡單路徑就是這樣一條路徑,其上的所有頂點都是互異的,但第一個頂點和最後一個頂點可能相同
-
環:如果圖含有一個頂點到它自身的邊(v,v),那麼路徑v,v叫做環。我們要討論的圖一般都是無環的。
-
有向圖的圈:滿足w1=wn,且至少長為1的一條路徑;如果該路徑是簡單路徑,那麼這個圈就是簡單圈。對於無向圖來說,我們要求邊是互異的。
-
DAG有向無環圖:如果一個有向圖沒有圈,那麼這個有向圖就是有向無環圖。
-
連通性:如果一個無向圖中從每個頂點到其他頂點都存在一個路徑,那麼這個無向圖就是連通的。
-
強連通性:如果一個有向圖中從每個頂點到其他頂點都存在一個路徑,那麼這個無向圖就是強連通的。
-
若連通性:如果一個有向圖不是強連通的,但是它的基礎圖(即其弧上去掉方向所形成的的圖,)是連通的。那麼該有向圖就是弱連通圖。
-
完全圖:圖中每一對頂點之間都存在一條邊的圖。
- 圖的表示
若圖是稠密的圖,我們就可以採用鄰接矩陣(使用一個二維陣列)來表示這個圖。對於每條邊(v,w),置A[u][v]等於true,否則這個陣列的元素就是false。如果變有一個權值,那麼就可以置這個權值為A[u][v]的值,而使用一個很大的值和一根小的值來表示不存在的邊。
如圖示稀疏的圖,我們就可以採用鄰接表來表示這個圖。對於每一個頂點,我們使用一個表來存放這個頂點所有鄰接的頂點。此時的空間需求為O(|E|+|V|)。
對於有幾種方式保留鄰接表。首先注意到的是,這些鄰接表本身可以被儲存在任何類的List中,即ArrayList和LinkedList中。然而,對於非常稀疏的圖來說,當使用ArrayList的時候,程式設計師可能需要從一個比預設更小的容量開始ArrayList;否則可能會造成空間的浪費
還有一種方式就是使用一個對映,在這個對映下,關鍵字就是那些頂點,而他們的值就是那些鄰接表,或者把一個鄰接表作為Vetex類的資料成員儲存下來。
9.2 拓撲排序
拓撲排序是對有向無圈圖的頂點的一種排序,使得如果存在一條從vi到vj的路徑,那麼排序中vi就出現在vj的後面。
一個簡單的拓撲排序的演算法就是先找出任意一個沒有入邊的頂點。然後顯示出該頂點,並將它從其邊中刪除。然後我們圖的其他部分同樣應用這樣的方法。
- 入度:頂點v的入度定義為(u,v)的條數
- 出度:頂點u的出度定義為(u,v)的條數。
9.3 最短路徑演算法
- 單源最短路徑
給定一個賦權圖G = (V,E) 和一個特定頂點s作為輸入,找出從s 到G中每一個其他頂點的最短賦權路徑。
9.3.1 無權最短路徑
廣度優先遍歷:該方法按層處理頂點:距開始最近的那些頂點首先被求值,而最遠的那些頂點最後被求值。這很像樹的層次遍歷。
使用對拓撲排序的同樣的分析,我們看到,只要是使用鄰接表,則執行時間就是O(|E|+|v|)。
9.3.2 Dijkstra演算法
如果是賦權圖,那麼問題無疑就變得非常困難了,不過我們仍然可以使用來自無權情形時的想法。
解決單源最短路徑的一般方法叫做Dijkstra演算法。Dijkstra演算法按照階段進行,正像無權最短路徑演算法一樣。在每個階段,Dijkstra演算法選擇一個頂點v,他在所有unknown頂點中具有最小的dv,同時演算法宣告從s到v的最短路徑是known的。階段的其餘部分由dw的更新工作組成。
下面我們給出Dijkstra演算法虛擬碼
Dijkstra演算法 實現
public class Dijkstra {
//建立地圖
/*
* S——16——>C—— 2——>D
* | \ ^ ^ ^
* 4 8 | \ |
* | \ 7 5 6
* v v | \ |
* A—— 3——>B—— 1——>E
*/
public static class Graph {
Map<Character,List<Node>> map=new HashMap<Character,List<Node>>();//輸出的地圖
public Graph() {
List<Node> list=new ArrayList<Node>();
list.add(new Node('S','A',4));
list.add(new Node('S','B',8));
list.add(new Node('S','C',16));
list.add(new Node('A','B',3));
list.add(new Node('B','C',7));
list.add(new Node('B','E',1));
list.add(new Node('C','D',2));
list.add(new Node('E','C',5));
list.add(new Node('E','D',6));
for(int i=0;i<list.size();i++) {
List<Node> temp = map.get(list.get(i).getSrc());
if(temp==null)
temp=new ArrayList<Node>();
temp.add(list.get(i));
map.put(list.get(i).getSrc(), temp);
}
}
}
public static class Node {
private Character src;//起點
private Character des;//終點
private int len; //距離長度
private int path=Integer.MAX_VALUE;//初始設定為無窮長
boolean known=false; //訪問過一次後就死亡
public Node(){}
public Node(Character src,Character des,int len) {
this.src=src;
this.des=des;
this.len=len;
}
void setPath(int path) {
this.path=path;
}
int getPath() {
return path;
}
Character getSrc() {
return src;
}
Character getDes() {
return des;
}
int getLen() {
return len;
}
}
public static Map<Character,Integer> dijkstra(Map<Character,List<Node>> map,Character c) {
Queue<Node> heap=new LinkedList<Node>();
//初始節點
Node root=new Node(c,c,0);
root.setPath(0);
heap.add(root);
Map<Character,Integer> result=new HashMap<Character,Integer>();
while(!heap.isEmpty()) {
Node x=heap.poll(),y = null;
List<Node> temp=map.get(x.getDes());
if(temp==null)
continue;
for(int i=0;i<temp.size();i++) {
y=temp.get(i);
if(y.getPath() > x.getDes()+y.getLen())
temp.get(i).setPath(x.getPath()+y.getLen());
if(!temp.get(i).known) {
heap.add(temp.get(i));
temp.get(i).known=true;
}
if(result.get(temp.get(i).getDes())==null) {
result.put(temp.get(i).getDes(),temp.get(i).getPath());
}
if(result.get(temp.get(i).getDes())>temp.get(i).getPath()) {
result.put(temp.get(i).getDes(),temp.get(i).getPath());
}
}
}
return result;
}
public static void main(String[] argc)
{
Dijkstra.Graph graph=new Dijkstra.Graph();
Map<Character,Integer> result=dijkstra(graph.map,'S');
for(Map.Entry<Character,Integer> entry:result.entrySet())
{
System.out.println("S-->"+entry.getKey()+" 長度"+entry.getValue());
}
}
}
9.3.3 具有負邊值的圖
如果圖負的邊值,那麼Dijkstra演算法是行不通的。問題在於,一旦一個頂點被宣告是known的,那麼就可能從某個另外的unknown頂點v有一條回到u的負值的路徑。
所以我們的解決方案就是,我們要忘記了關於unknown的頂點的概念,因為我們的演算法要能夠改變它的意向,下面是具有負值的賦值最短路徑的虛擬碼:
9.3.4 無圈圖
如果我們知道圖是無圈的,那麼我們可以通過改變宣告頂點known的順序,或者叫作頂點選取法則,來改進Dijkstra演算法。新法則是以拓撲順序選擇頂點。由於選擇和更新可以在拓撲排序執行的時候進行,因此演算法可以一趟完成。
因為當一個頂點被選取以後,按照拓撲排序的法則它沒有從known頂點發出的進入邊,因此它的距離dv可以不再被降低,所以這種選取法則是行的通的。
無圈圖的一個更重要的應用就是關鍵路徑法。我們用如下動作節點圖作為一個例子。每個節點表示一個必須執行的動作以及完成動作所要花費的時間。因此,該圖叫做動作節點圖。
9.4 最小生成樹問題
最小生成樹問題:一個無向圖的最小生成樹就是由該圖的那些連線G的所有頂點的邊構成的樹,且總價值是最小的。
上面圖中最小生成樹種的邊的條數為(|V|-1)
9.4.1 Prim演算法
演算法思想:在演算法的任意時刻,我們都可以看到一組已經新增到樹上的頂點,而其餘頂點尚未新增到這個樹中。此時,演算法在每一個階段都可以通過(u,v)使得(u,v)中的值是所以u在樹上但v不在樹上的邊的值中的最小者而找出一個新的頂點並把它新增到這可樹中。
這個演算法的實現和Dijkstra演算法是一樣的,對於Dijkstra演算法分析所做的一件事都可以用到這裡。
9.4.2 Kruskal演算法
Kruskal演算法的貪婪策略就是聯絡按照最小的權值選擇邊,並且當所選的邊不產生圈的時候就把他作為所選取的邊。
形式上,Kruskal演算法就是在處理一個深林----樹的集合。開始的時候,存在|V|顆單節點的樹,而新增一邊就可以將兩顆樹合併成一顆樹。當演算法終止的時候,就只有一顆樹了,這棵樹就是最小生成樹。
9.5 深度優先搜尋的應用
深度優先遍歷是對先序遍歷的推廣。我們從某個頂點u開始出來v,然後遞迴地開始遍歷所有與v鄰居的頂點。我們對任意的圖進行該過程,那麼我們要小心仔細的避免圈的出現。為此,當訪問一個頂點v的時候,由於我們當時已經到了該點處,因此可以標記該點是訪問過的,並且對於尚未被標記的所有鄰居頂點來說遞迴呼叫深度優先遍歷。
9.5.1 無向圖
深度優先搜尋的虛擬碼: