1. 程式人生 > >圖的概念和關於圖的幾個演算法

圖的概念和關於圖的幾個演算法

一、圖的概念

圖是演算法中是樹的拓展,樹是從上向下的資料結構,結點都有一個父結點(根結點除外),從上向下排列。而圖沒有了父子結點的概念,圖中的結點都是平等關係,結果更加複雜。

圖G=(V, E),其中V代表頂點Vertex,E代表邊edge,一條邊就是一個定點對(u,v),其中(u,v)∈V。

          

              無向圖 有向圖

圖分有向圖和無向圖。在無向圖中,如果(u,v) (表示u到v的路徑)聯通,那麼(v,u)也聯通,例如“1”到“2”聯通,“2”到“1”也聯通。但是在又向圖中“1”到“2”聯通,但是“2”到“1”是不聯通的。

在圖的概念中,除了頂點和邊的概念外,經常還涉及到權值,表示一個頂點到另一個頂點的的“代價”,如果頂點不聯通,可以認為權值無限大。如果不涉及權值,那麼可以認為聯通的頂點權值都為1.

在資料結構中,經常用鄰接表和鄰接矩陣表示圖。

1、鄰接表


上圖即為有向圖的鄰接表,表中的一個結點對應圖中的一個結點,結點後面的連結串列是與這個結點聯通的結點。
//圖的結點
typedef struct Node{
	char value;// 結點
	Node *next;//指向聯通結點
};
//鄰接表
Node Adj[Num];//Num為圖結點個數

鄰接表常用語表示稀疏圖,即結點的邊數|E|遠小於|V|^2。 對於有向圖,鄰接表儲存所佔空間為|V|+|E|,對於無向圖|V|+2|E|,因為每條表在鄰接表中出現兩次。在儲存上佔優勢,但是在判斷兩個結點(u,v)是否聯通時,要首先在鄰接表中找到u,遍歷u後面的連結串列才能判斷。

2、鄰接矩陣


上圖是無向圖的鄰接矩陣表示。鄰接矩陣是一個|V|x|V|的矩陣GMatr,如果(u,v)聯通,那麼GMatr[u][v]=1。 如果圖是加權的話,GMatr[u][v]=權值。
bool GMatr[Num][Num];//Num為圖結點個數

可以看出,鄰接矩陣的表示方法佔得空間為O(V^2),但是在判斷兩個結點是否聯通時,只需O(1)。 當圖比較小時更多采用鄰接矩陣,因為它更明瞭。如果圖沒有加權時,可以用一個二進位制位來表示兩個圖是否聯通。

二、圖的演算法

1、拓撲排序

拓撲排序是應用於有向無環圖的一種排序。例如在圖中存在u到v的通路(未必聯通)那麼排序後,u出現在v前面。 拓撲排序常常用在有關係的工程之間的排序。例如在一個解決方案中,工程A完工後才可進行工程B工程C,而工程D又依賴於工程B和工程C。

那麼排序後可能為A B C D,也可能為A C B D。 首先定義“入度(indgree)”這一概念,v的入度為所有(u,v)聯通結點的個數,即可以從多少個結點直接到v。 那麼拓撲排序步驟如下: 1、從圖找找出任一一個入度為零的結點,依序放到排序佇列。 2、在圖中刪除該結點,並且刪除從該結點出發的邊。 3、更新其他結點的入度。 重複1-3,直到所有結點都已排序。 程式碼:
void Topsort()
{
	for(int i=0;i<Num;i++)
	{
		Vertex v=findIndegreeZero();//找到任一一個入度為零的點
		putVerter(v);//把結點放到排序佇列
		for each Vertex u adjanct to v//v到w結點聯通
			w.indgree--;//入度減一
		}
}

2、圖的廣度優先遍歷

圖的廣度優先遍歷有點像樹的層次遍歷,是一個分層搜尋的過程。 假設從v0結點開始遍歷,首先遍歷與v0結點聯通的點w1,w2……,再遍歷與w1聯通的點u1,u2……,與w2聯通的點q1……。在遍歷過程中,要注意不要重複遍歷一個結點,往往在遍歷過一個結點後就對這個結點做標記。 廣度優先遍歷常常藉助佇列。步驟如下: 1、把結點v放入佇列。標記v 2、若佇列為空則結束,否則取出佇列頭結點u。 3、找出與u聯通的結點w1,w2……,若未遍歷則遍歷,然後標記、入隊。轉到2。
void BFS()
{
	Vertex v;//最先遍歷的結點
	v.visit=true;//標記
	Q.push(v);//入隊
	while(!Q.empty())
	{
		Vertex u=pop();
		foreach(u的每一個臨界點 w)
		{
			w.visit=true;
			Q.push(w);
		}
	}
}

3、圖的深度優先遍歷

深度優先遍歷是儘可能“深"的遍歷圖。假設從結點v0開始遍歷,遍歷與v0聯通的且未必遍歷過的結點v1,再遍歷與v1聯通的且未被遍歷過的結點v2……。如果遍歷到vn後無結點可以遍歷,那麼退回的哦v(n-1)再去找結點遍歷,依次類推。直到圖中所有結點都被遍歷過。 可以看出圖的深度優先遍歷可以藉助堆疊。 1、把結點v放入堆疊。標記v 2、若堆疊為空則結束,否則取出棧頂結點u。 3、找出與u聯通的且未被標記的結點w1,w2……,併入棧。轉到2。
void DFS()
{
	Vertex v;
	v.visit=true;
	S.push(v);
	while(!S.empty())
	{
		Vertex u=S.pop();
		u.visit=true;
		for(u的每一個鄰接點 w and w.visit=false)
					S.push(w);
	}
}

4、最小生成樹

最小生成樹應用於無向聯通圖。 生成樹是指把圖中所有的結點連線起來,任一兩個頂點之間有通路。最小是指把所有頂點連線起來的路徑的權值的和最小。 構建最小生成樹是通過貪心演算法來構建,通過區域性最優來達到整體最優。 G(V,E)是一個無向聯通圖,其權值函式為w。 A是最小生成樹的子集,初始為空;通過迴圈迭代,每次往A中加入一條邊,且確保加入邊後,A仍是最小生成樹的子集,那麼加入的這條邊就叫做“安全邊(safe edge)”。直到把所有的結點都加入到A中,迴圈結束。
Greec-MST(G,w)
{
	A=∅;//空集合
	while A don't for a spanning tree
		do find a edge(u,v) that is safe for A
		A=A∪{(u,v)};
  return A;
}
在集合A和安全邊的選擇上,不同的方法形成了兩個最小生成樹演算法:1、Kruskal。2、Prim。 這兩個演算法都使用二叉堆的話,演算法複雜度為O(ElogV),但是如果Prim使用斐波那契堆的話,其演算法複雜度可以達到O(E+VlogV)。

1、Kruskal演算法

在Kruskal演算法中,A是一顆森林,把權值排序選取權值最小的邊,若選取的邊不形成迴路,則為安全邊,把它新增的正在生長的森林中。
MST-KRUSKAL(G,w)
{
	A=∅;//空集
	for each v in V
	 do make-set(v);//把v做成集合
	sort the edges of E into nondecreasing order by weight w//安裝權值升序排列
	for each (u,v) in E, taken in nondecreasing order by weight  
         if Find-Set(u) != Find-Set(v) //Find-Set(v)為找出v所在集合的代表元素
          A = A U {(u,v)}  
          Union(u,v)  //合併兩個集合
  return A;
}

2、Prim演算法

在Prim演算法中,A中的邊形成單樹,每次迴圈向A中新增一個結點(權值最小的邊連線的結點)。在演算法實現中用到一個最小優先順序佇列,不在樹中的結點多放在基於權值key的的最小優先順序佇列Q中,對於結點v來說,key[v]的值是與樹A中某一頂點連線的某一條邊的最小權值,如果不連線,那麼key[v]=∞。
MST-PRIM(G,w)
{
	//選取一個頂點v
	Vertex v;
	key[v]=0;
	G=G-{v};//集合中減去v
	foreach u∈G
		do key[u]=∞;
	Q=G;//構造優先順序佇列
	while(Q!=?)
		do u=EXTRACT-MIN(Q)
			foreach u∈Adj[v]
				do if u∈Q and w(v,u)<key[u]
				   key[u]=w(v,u)
}