1. 程式人生 > >資料結構——圖論(java)

資料結構——圖論(java)

一.基本概念

1、頂點(vertex)

表示某個事物或物件。由於圖的術語沒有標準化,因此,稱頂點為點、節點、結點、端點等都是可以的。

2、邊(edge)

通俗點理解就是兩個點相連組合成一條邊,表示事物與事物之間的關係。需要注意的是邊表示的是頂點之間的邏輯關係,粗細長短都無所謂的。包括上面的頂點也一樣,表示邏輯事物或物件,畫的時候大小形狀都無所謂。

3、同構(Isomorphism )

先看看下面2張圖:
這裡寫圖片描述這裡寫圖片描述
首先你的感覺是這2個圖肯定不一樣。但從圖(graph)的角度出發,這2個圖是一樣的,即它們是同構的。前面提到頂點和邊指的是事物和事物的邏輯關係,不管頂點的位置在哪,邊的粗細長短如何,只要不改變頂點代表的事物本身,不改變頂點之間的邏輯關係,那麼就代表這些圖擁有相同的資訊,是同一個圖。同構的圖區別僅在於畫法不同。

4、有向/無向圖(Directed Graph/ Undirected Graph)

最基本的圖通常被定義為“無向圖”,與之對應的則被稱為“有向圖”。兩者唯一的區別在於,有向圖中的邊是有方向性的。有向圖和無向圖的許多原理和演算法是相通的。

5、權重(weight)

邊的權重(或者稱為權值、開銷、長度等),也是一個非常核心的概念,即每條邊都有與之對應的值。例如當頂點代表某些物理地點時,兩個頂點間邊的權重可以設定為路網中的開車距離。有時候為了應對特殊情況,邊的權重可以是零或者負數,也別忘了“圖”是用來記錄關聯的東西,並不是真正的地圖。

6、路徑/最短路徑(path/shortest path)

在圖上任取兩頂點,分別作為起點(start vertex)和終點(end vertex),我們可以規劃許多條由起點到終點的路線。不會來來回回繞圈子、不會重複經過同一個點和同一條邊的路線,就是一條“路徑”。兩點之間存在路徑,則稱這2個頂點是連通的(connected)。
還是上圖的例子,北京->上海->廣州,是一條路徑,北京->武漢->廣州,是另一條路徑,北京—>武漢->上海->廣州,也是一條路徑。而北京->武漢->廣州這條路徑最短,稱為最短路徑。
路徑也有權重。路徑經過的每一條邊,沿路加權重,權重總和就是路徑的權重(通常只加邊的權重,而不考慮頂點的權重)。在路網中,路徑的權重,可以想象成路徑的總長度。在有向圖中,路徑還必須跟隨邊的方向。
值得注意的是,一條路徑包含了頂點和邊,因此路徑本身也構成了圖結構,只不過是一種特殊的圖結構。

7、環(loop)

環,也成為環路,是一個與路徑相似的概念。在路徑的終點新增一條指向起點的邊,就構成一條環路。通俗點說就是繞圈。與路徑一樣,有向圖中的環路也必須跟隨邊的方向。環本身也是一種特殊的圖結構。

8、連通圖/連通分量(connected graph/connected component)

如果在圖G中,任意2個頂點之間都存在路徑,那麼稱G為連通圖(注意是任意2頂點)。上面那張城市之間的圖,每個城市之間都有路徑,因此是連通圖。而下面這張圖中,頂點8和頂點2之間就不存在路徑,因此下圖不是一個連通圖,當然該圖中還有很多頂點之間不存在路徑。
這裡寫圖片描述
上圖雖然不是一個連通圖,但它有多個連通子圖:0,1,2頂點構成一個連通子圖,0,1,2,3,4頂點構成的子圖是連通圖,6,7,8,9頂點構成的子圖也是連通圖,當然還有很多子圖。我們把一個圖的最大連通子圖稱為它的連通分量。0,1,2,3,4頂點構成的子圖就是該圖的最大連通子圖,也就是連通分量。連通分量有如下特點:
1)是子圖;
2)子圖是連通的;
3)子圖含有最大頂點數。
注意:“最大連通子圖”指的是無法再擴充套件了,不能包含更多頂點和邊的子圖。0,1,2,3,4頂點構成的子圖已經無法再擴充套件了。
顯然,對於連通圖來說,它的最大連通子圖就是其本身,連通分量也是其本身。

二.圖的兩種表示形式

1、鄰接表

鄰接表的核心思想就是針對每個頂點設定一個鄰居表。

這裡寫圖片描述
以上面的圖為例,這是一個有向圖,分別有頂點a, b, c, d, e, f, g, h共8個頂點。使用鄰接表就是針對這8個頂點分別構建鄰居表,從而構成一個8個鄰居表組成的結構,這個結構就是我們這個圖的表示結構或者叫儲存結構。

a, b, c, d, e, f, g, h = range(8)
N = [{b, c, d, e, f},  # a 的鄰居表
     {c, e},  # b 的鄰居表
     {d},  # c 的鄰居表
     {e},  # d 的鄰居表
     {f},  # e 的鄰居表
     {c, g, h},  # f 的鄰居表
     {f, h},  # g 的鄰居表
     {f, g}]  # h 的鄰居表

個人覺得在java裡面用map儲存比較好,key用來儲存定點,value用來儲存與頂點相關的其他頂點集合。

2、鄰接矩陣

(2)鄰接矩陣

鄰接矩陣的核心思想是針對每個頂點設定一個表,這個表包含所有頂點,通過True/False來表示是否是鄰居頂點。
還是針對上面的圖,分別有頂點a, b, c, d, e, f, g, h共8個頂點。使用鄰接矩陣就是針對這8個頂點構建一個8×8的矩陣組成的結構,這個結構就是我們這個圖的表示結構或儲存結構。

在鄰接矩陣表示法中,有一些非常實用的特性。

  • 首先,可以看出,該矩陣是一個方陣,方陣的維度就是圖中頂點的數量,同時還是一個對稱矩陣,這樣進行處理時非常方便。
  • 其次,該矩陣對角線表示的是頂點與頂點自身的關係,一般圖不允許出現自關聯狀態,即自己指向自己的邊,那麼對角線的元素全部為0;
  • 最後,該表示方式可以不用改動即可表示帶權值的圖,直接將原來儲存1的地方修改成相應的權值即可。當然, 0也是權值的一種,而鄰接矩陣中0表示不存在這條邊。出於實踐中的考慮,可以對不存在的邊的權值進行修改,將其設定為無窮大或非法的權值,如None,-99999/99999等

三.廣度優先搜尋(BFS)

BFS使用佇列(queue)來實施演算法過程,佇列(queue)有著先進先出FIFO(First Input First Output)的特性,BFS操作步驟如下:
1、把起始點放入queue;
2、重複下述2步驟,直到queue為空為止:
1) 從queue中取出佇列頭的點;
2) 找出與此點鄰接的且尚未遍歷的點,進行標記,然後全部放入queue中

具體流程圖可參考:https://blog.csdn.net/saltriver/article/details/54428983

四.深度優先搜尋(DFS)

DFS的實現方式相比於BFS應該說大同小異,只是把queue換成了stack而已,stack具有後進先出LIFO(Last Input First Output)的特性,DFS的操作步驟如下:
1、把起始點放入stack;
2、重複下述3步驟,直到stack為空為止:

  • 從stack中訪問棧頂的點;
  • 找出與此點鄰接的且尚未遍歷的點,進行標記,然後全部放入stack中;
  • 如果此點沒有尚未遍歷的鄰接點,則將此點從stack中彈出。

具體流程圖可參考:https://blog.csdn.net/saltriver/article/details/54429068

五.最小生成樹

1.Prim演算法

MST(Minimum Spanning Tree,最小生成樹)問題有兩種通用的解法,Prim演算法就是其中之一,它是從點的方面考慮構建一顆MST,大致思想是:設圖G頂點集合為U,首先任意選擇圖G中的一點作為起始點a,將該點加入集合V,再從集合U-V中找到另一點b使得點b到V中任意一點的權值最小,此時將b點也加入集合V;以此類推,現在的集合V={a,b},再從集合U-V中找到另一點c使得點c到V中任意一點的權值最小,此時將c點加入集合V,直至所有頂點全部被加入V,此時就構建出了一顆MST。因為有N個頂點,所以該MST就有N-1條邊,每一次向集合V中加入一個點,就意味著找到一條MST的邊。

用圖示和程式碼說明:

初始狀態:

設定2個數據結構:

lowcost[i]:表示以i為終點的邊的最小權值,當lowcost[i]=0說明以i為終點的邊的最小權值=0,也就是表示i點加入了MST

mst[i]:表示對應lowcost[i]的起點,即說明邊<mst[i],i>是MST的一條邊,當mst[i]=0表示起點i加入MST

 

我們假設V1是起始點,進行初始化(*代表無限大,即無通路):

 

lowcost[2]=6,lowcost[3]=1,lowcost[4]=5,lowcost[5]=*,lowcost[6]=*

mst[2]=1,mst[3]=1,mst[4]=1,mst[5]=1,mst[6]=1,(所有點預設起點是V1)

 

明顯看出,以V3為終點的邊的權值最小=1,所以邊<mst[3],3>=1加入MST

此時,因為點V3的加入,需要更新lowcost陣列和mst陣列:

 

lowcost[2]=5,lowcost[3]=0,lowcost[4]=5,lowcost[5]=6,lowcost[6]=4

mst[2]=3,mst[3]=0,mst[4]=1,mst[5]=3,mst[6]=3

 

明顯看出,以V6為終點的邊的權值最小=4,所以邊<mst[6],6>=4加入MST

 

此時,因為點V6的加入,需要更新lowcost陣列和mst陣列:

 

lowcost[2]=5,lowcost[3]=0lowcost[4]=2,lowcost[5]=6,lowcost[6]=0

mst[2]=3,mst[3]=0,mst[4]=6,mst[5]=3,mst[6]=0

 

 

明顯看出,以V4為終點的邊的權值最小=2,所以邊<mst[4],4>=4加入MST

 

此時,因為點V4的加入,需要更新lowcost陣列和mst陣列:

 

lowcost[2]=5,lowcost[3]=0lowcost[4]=0,lowcost[5]=6,lowcost[6]=0

mst[2]=3,mst[3]=0mst[4]=0,mst[5]=3,mst[6]=0

 

明顯看出,以V2為終點的邊的權值最小=5,所以邊<mst[2],2>=5加入MST

 

此時,因為點V2的加入,需要更新lowcost陣列和mst陣列:

 

lowcost[2]=0,lowcost[3]=0,lowcost[4]=0,lowcost[5]=3lowcost[6]=0

mst[2]=0,mst[3]=0,mst[4]=0,mst[5]=2,mst[6]=0

 

很明顯,以V5為終點的邊的權值最小=3,所以邊<mst[5],5>=3加入MST

 

lowcost[2]=0,lowcost[3]=0,lowcost[4]=0,lowcost[5]=0,lowcost[6]=0

mst[2]=0,mst[3]=0,mst[4]=0,mst[5]=0,mst[6]=0

 

至此,MST構建成功,如圖所示:

int graph[MAX][MAX];
 
int prim(int graph[][MAX], int n)
{
	int lowcost[MAX];
	int mst[MAX];
	int i, j, min, minid, sum = 0;
    //將與第一個點相連的邊的長度都遍歷到lowcost陣列中
	for (i = 2; i < n; i++)
	{
		lowcost[i] = graph[0][i];
		mst[i] = 1;
	}
	mst[1] = 0;
	for (i = 2; i < n; i++)
	{
		min = MAXCOST;
		minid = 0;
        //找出最小的一條邊
		for (j = 2; j < n; j++)
		{
			if (lowcost[j] < min && lowcost[j] != 0)
			{
				min = lowcost[j];
				minid = j;
			}
		}		
        system.out.println("V"+mst[minid]+"-V"+minid+"="+min);
		sum += min;
        //將已經選中過的邊置為0,下次篩選的時候會過濾
		lowcost[minid] = 0;
        //遍歷一遍lowcost陣列,倘若新加入終點的邊與lowcost中相同終點的邊的值更小,則替換
		for (j = 2; j < n; j++)
		{
			if (graph[minid][j] < lowcost[j])
			{
				lowcost[j] = graph[minid][j];
				mst[j] = minid;
			}
		}
	}
	return sum;
}

轉載:https://blog.csdn.net/yeruby/article/details/38615045

2.Kruskal演算法

1、Kruskal演算法描述

      Kruskal演算法是基於貪心的思想得到的。首先我們把所有的邊按照權值先從小到大排列,接著按照順序選取每條邊,如果這條邊的兩個端點不屬於同一集合,那麼就將它們合併,直到所有的點都屬於同一個集合為止。至於怎麼合併到一個集合,那麼這裡我們就可以用到一個工具——-並查集(不知道的同學請移步:Here)。換而言之,Kruskal演算法就是基於並查集的貪心演算法。

2、Kruskal演算法流程

      對於圖G(V,E),以下是演算法描述:

  1. 輸入: 圖G

  2. 輸出: 圖G的最小生成樹

  3. 具體流程:

  4. (1)將圖G看做一個森林,每個頂點為一棵獨立的樹

  5. (2)將所有的邊加入集合S,即一開始S = E

  6. (3)從S中拿出一條最短的邊(u,v),如果(u,v)不在同一棵樹內,則連線u,v合併這兩棵樹,同時將(u,v)加入生成樹的邊集E'

  7. (4)重複(3)直到所有點屬於同一棵樹,邊集E'就是一棵最小生成樹

 

 

 

      我們用現在來模擬一下Kruskal演算法,下面給出一個無向圖B,我們使用Kruskal來找無向圖B的最小生成樹。

 

        首先,我們將所有的邊都進行從小到大的排序。排序之後根據貪心準則,我們選取最小邊(A,D)。我們發現頂點A,D不在一棵樹上,所以合併頂點A,D所在的樹,並將邊(A,D)加入邊集E‘。

         我們接著在剩下的邊中查詢權值最小的邊,於是我們找到的(C,E)。我們可以發現,頂點C,E仍然不在一棵樹上,所以我們合併頂點C,E所在的樹,並將邊(C,E)加入邊集E'

       不斷重複上述的過程,於是我們就找到了無向圖B的最小生成樹,如下圖所示:

3、Kruskal演算法的時間複雜度

      Kruskal演算法每次要從都要從剩餘的邊中選取一個最小的邊。通常我們要先對邊按權值從小到大排序,這一步的時間複雜度為為O(|Elog|E|)。Kruskal演算法的實現通常使用並查集,來快速判斷兩個頂點是否屬於同一個集合。最壞的情況可能要列舉完所有的邊,此時要迴圈|E|次,所以這一步的時間複雜度為O(|E|α(V)),其中α為Ackermann函式,其增長非常慢,我們可以視為常數。所以Kruskal演算法的時間複雜度為O(|Elog|E|)。

作者程式碼的實現可以新增一個節點結束器,每聯合一個節點就加一,當節點數達到圖中的總節點數的時候,就break跳出迴圈;

//自己寫的,程式碼為測試,有問題歡迎指出
class Kruskal{
   int nodeNum;
   List<Side> listSide;

   public Kruskal(String[] node,int[][] graph){
        nodeNum = node.length;
		//去除重複,取二維陣列的斜三角
		for(int i=1;i<nodeNum;i++){
		   for(int j=0;j<i;j++){
		       if(graph[i][j]>0){
			        Side side = new Side();
					side.start = i;
					side.end = j;
					side.length = graph[i][j];
					listNode.add(side);
			   }
		   }
		}
   }
  
  //定義邊
   class Side{
      private int start;
	  private int end;
	  private int length;	  	  
   }
   
   public void init(){
       this.father = new int[nodeNum];
	   for(int i=0;i<nodeNum;i++){
	      father[i] = -1;
	   }
   }
   
   public int find(int x){
	   if(father[X] <= 0)
		return X;
	   else
		return father[X] = find(father[X]);
	   
   }
   
   public void union(int root1,int root2){
    if (father[root2]<father[root1]){
	      father[root1] = root2;
	  }else{
	    if (father[root1]==father[root2])
	      father[root1] --;
		 father[root2] = root1;	
	  }	  
   }
   
   

   public int printKruskal(){
      //將邊按長度從小到大排序
      Sort(listSide);
	  int currentNodeNum = 1;
	  int sum = 0;
	  for(int i=0;i<listSide.size();i++){
	      int startNode = listSide.get(i).start;
		  int endNode = listSide.get(i).end;
		  //這裡使用交併集解決會連城圈的問題
	      if(find(startNode) == -1 && find(endNode) == -1){
		      union(startNode,endNode);
			  nodeNum ++;
			  sum += listSide.get(i).length;
		  }else if(find(startNode) !=  find(endNode)){
		      union(startNode,endNode);
			  nodeNum ++;
			  sum += listSide.get(i).length;
		  }
		  
		  if(currentNodeNum == nodeNum) break;
	  }
   }
}

轉載:https://blog.csdn.net/luomingjun12315/article/details/47700237

6.最短路徑

程式碼好理解:https://blog.csdn.net/qibofang/article/details/51594673

文字好理解:https://blog.csdn.net/tianjing0805/article/details/76023080