PHP資料結構(十一) ——圖的連通性問題與最小生成樹演算法(1)
PHP資料結構(十一)——圖的連通性問題與最小生成樹演算法(1)
(原創內容,轉載請註明來源,謝謝)
一、連通分量和生成樹
1、無向圖
設E(G)為連通圖G的所有邊的集合,從圖的任意一點出發遍歷圖,可以將E(G)分為T(G)和B(G),T表示已經遍歷過的邊的集合,B表示剩餘邊的集合。因此,T與圖G的所有頂點構成的極小連通子圖,就是G的一棵生成樹。由深度優先搜尋的稱為深度優先生成樹;由廣度優先搜尋的稱為廣度優先生成樹。
2、有向圖
有向圖和無向圖類似。有向圖的強連通分量,是對圖進行深度優先遍歷,遍歷完成後,從被遍歷的最後一個節點開始,做逆向的深度優先遍歷。
二、關節點和重連通分量
1、定義
1)當刪去圖的節點V以及和B相關聯的各邊後,圖若被分割成兩個或以上的圖,則稱V為關節點。
2)一個沒有關節點的連通圖,稱為重連通圖。
3)刪去k個節點後,才會破壞圖的連通性,則該圖的連通度為k。
2、獲取方式
圖的關鍵點數量可以用深度優先搜尋的方法獲取。
關節點主要有以下兩個特性:
1)若生成樹的根有兩個以上的子節點,則此根為關節點。
2)若生成樹的某個非葉子節點V,其若干棵子樹以及子樹的子節點均沒有指向V的祖先的回邊,則V即為關節點。
3)關節點至少要與兩個節點相連(如果只和一個節點相連,則是葉子節點,其是否斷開不影響圖的連通性)。
根據以上兩個特性,主要判斷方式如下:
1)資料結構利用上一篇將圖遍歷時候的節點結構
class Node{
public $val = null;
public $arrNext =array();//儲存下一個節點位置的陣列
}
2)遍歷每一個節點,對節點進行篩選,以下三種情況,V不是關節點,其餘V是關節點。
A. 當節點V的arrNext陣列小於或等於1時,V為葉子節點或為單獨的節點,則V不是關節點;
B. 如果V的某個子節點vi,是V其他子節點vk的子節點vx(不含V)的子節點,則V不是關節點;
C.如果V的某個子節點vi的子節點vk(不含V),是V其他子節點 vx(不含vk)的子節點,則V不是關節點。
否則,節點V是關節點。
三、最小生成樹
1、場景
現假設需要對一個城市的某幾個區域進行通訊網路的連線,每兩個點之間有自己的耗費,現需要把這幾個點連線起來行程通訊網,又想要最節省耗費。
將每個區域看成一個節點,區域之間看成無向圖的邊,每兩個點之間的耗費看成邊的權,則該問題化簡為求一個無向圖考慮到權值情況下的的最小生成樹。
2、概念
1)生成樹的代價:各邊權值的和,代價最小時稱為最小生成樹。
2)MST:最小生成樹的性質,假設連通網N=(V, {E}),U是頂點集V的一個非空子集,若(u, v)是一條具有最小權值的邊,其中u屬於U,v屬於V-U,則必存在一棵包含點(u, v)的最小生成樹。
3)最小生成樹有兩種演算法,一種叫做普里姆(Prim)演算法,一種叫做克魯斯卡爾(Kruskal)演算法。
(PS:本來寫文章時,是將Prim和Kruskal演算法放在一篇的,也便於比較,但是微信公眾號有個限制3000字的規定,所以我只能把Kruskal演算法挪到下一篇,見諒。)
3、Prim演算法
1)該演算法的時間複雜度為O(n2),即其時間複雜度和邊的數目無關,僅和頂點數目有關,適用於邊數較多的稠密網。
2)演算法內容
假設N={V, {E}}是連通網,TE是N上最小生成樹的集合。演算法從U={u0}(即圖上任意一點),TE={}開始,重複執行以下操作:
在所有的u屬於V,v屬於V-U的邊(u, v)屬於E,著一條代價最小的邊(u0,v0),併入集合TE,v0併入U,直到U=V為止。則TE包含n-1條邊,T=(V, {TE})是最小生成樹。
該演算法需要引入一個二維陣列,記錄任意兩個頂點之間的權值,如果兩個頂點沒有連線,則權值為無窮大。
4、Kruskal 挪至下一篇文章描述,原因見上述 斜體字。
5、總結
Prim演算法和Kruskal演算法,區別在於從頂點切入還是從邊切入。因此,當頂點較多但邊相對較少時,可以使用Kruskal演算法;反之,頂點較少而邊相對較多時,可以使用Prim演算法。兩個演算法都需要引入一個二維陣列,用於儲存任意兩點間的權值,當兩點沒有連線時,權值為無窮大,表示該點無法直接到達另一點。
6、編碼實現
PHP實現Prim演算法和Kruskal演算法的執行結果如下:
原始碼如下:
<?php
class MinTree{
public$siteWeigh = array();
publicfunction __construct($data = null){
if($data!= null){
$this->siteWeigh= $data;
}else{
//構造二維陣列,存放任意兩點間的權值
//由於是無向圖,因此具有軸對稱性
//用999表示兩個點沒有連線
$this->siteWeigh= array(
0=> array(999, 10, 15, 999, 20),
1=> array(10, 999, 999, 10, 5),
2=> array(15, 999, 999, 25, 999),
3=> array(999, 10, 25, 999, 10),
4=> array(20, 5, 999, 10, 999)
);
}
}
//Prim演算法:以頂點為依據生成最小生成樹
publicfunction getPrimResult(){
$arrTree= $this->siteWeigh;
$allKeys= array_keys($arrTree);//獲取全部節點
$nodeNum= count($allKeys);
$nodeStack= array();//用於存放已經被納入結果集的節點
array_push($nodeStack,0);
$resStack= array();//用於存放結果路徑,格式0=>ij,1=>jk
$curNodeNum= count($nodeStack);
//Prim演算法中獲取所有節點後即完成演算法
while($curNodeNum< $nodeNum){
$curMin= 999;
$tmpArr= array();//暫存中間結果資訊
$k= 0;//暫存節點替換資訊,一輪內如果有新的權值更小的節點,則原節點出棧
foreach($nodeStackas $curNode){//每次foreach從結果集中取一個點
for($i=0;$i<$nodeNum;$i++){
if(in_array($curNode,$nodeStack) && in_array($i, $nodeStack)){
continue;//如果兩個節點都已經在結果集,則進入下一輪迴圈
}
if($arrTree[$curNode][$i]< $curMin){
if($k> 0){
//nodestack是用於存 結果集的,因此如果再次進到這個if,
//說明上次進結果集的是暫存的內容,需要清除
array_pop($nodeStack);
}
$curMin= $arrTree[$curNode][$i];
$tmpArr[$curMin]= $curNode.$i;//拼接成ij的形式,便於確定是哪條邊
array_push($nodeStack,$i);
$k++;
}
}
}
array_push($resStack,$tmpArr[$curMin]);
//計數確認當前是否已經捕獲所有節點
$curNodeNum= count($nodeStack);
}
//計算路徑值
$sum= 0;
foreach($resStackas $key => $val){
$i= intval($val[0]);
$j= intval($val[1]);
$sum+= $arrTree[$i][$j];
}
returnarray('resRoad' => $resStack, 'resSum' => $sum);
}
}
$minTree = new MinTree();
$primRes = $minTree->getPrimResult();
echo '採用Prim演算法,獲取的最小生成樹為:';
print_r($primRes['resRoad']);
echo '<br />最終的權值和為:'.$primRes['resSum'].'<br/>';
題外話:兩種最小生成樹演算法,Prim以節點為切入點獲取最小生成樹,Kruskal以邊為切入點獲取最小生成樹。兩者實現方式較為不同,Prim演算法主要以棧的思想進行解決,因此實際編碼過程中進出棧的處理邏輯需要理清楚;Kruskal重在排序,當每條邊的長度排好時,其他問題迎刃而解。
——written by linhxx 2017.07.09