貪心演算法(四)——最小代價生成樹
問題描述
n個村莊間架設通訊線路,每個村莊間的距離不同,如何架設最節省開銷?
這個問題中,村莊可以抽象成節點,村莊之間的距離抽象成帶權值的邊,要求最節約的架設方案其實就是求如何使用最少的邊、最小的權值和將圖中所有的節點連線起來。
這就是一個最小代價生成樹的問題,可以用Prim演算法或kruskal演算法解決。
- PS1:無向連通圖的生成樹是一個極小連通子圖。
- PS2:生成樹是圖的一個子圖,包括所有的頂點和最少的邊(n-1條邊)。
- PS3:最小代價生成樹就是所有生成樹中權值之和最小的那個。
演算法思路
演算法的目標很明確,就是要在n個節點的圖中,找出n-1個節點,並且節點之間連線的權值是最小的。因此總體思路如下:
/**
* @param a:圖的鄰接矩陣
*/
EdgeSet spanningTree(int[][] a){
// 結果集(邊的集合)
EdgeSet solution = new EdgeSet();
// 選出n-1條邊
int i = 0;
while( i<n && 還有未檢查的邊 ){
// 選出一條邊
Edge edge = Select(a);
// 判斷是否有迴路
if ( !hasLoop(edge) ) {
solution.add( edge );
}
}
return solution;
}
上述為最小代價生成樹的總體思路,其中選邊方式(貪心準則)的不同,就產生不同的最小代價生成樹演算法。
圖的鄰接表示法
邊節點
一個邊節點有一條邊 和 一個終止節點組成。
/**
* 邊節點(由一條邊和一個終止節點構成)
*/
class ENode{
int id;// 終止節點的編號
int weight;// 邊的權重
}
圖的鄰接表示
圖用一個Map< String,List>表示,其中String表示節點的編號,List中儲存以該節點為起點的所有邊節點。
Map<String,List<ENode>>
Prim演算法
貪心準則:將整個圖分成兩部分,一部分已選入生成樹,另一部分在生成樹之外。每次選的邊要求一頭在生成樹之內,一頭在生成樹之外,並保證當前邊是滿足上述條件中最短的一條。重複上述操作,直到選出n-1條邊為止。
資料結構
mark:
Map<String,Boolean> mark = new HashMap<>();
記錄指定節點是否已在生成樹中。
key表示節點編號,value為boolean型,表示是否已選入生成樹中。nearest:
Map<String,String> nearest;
用於記錄最小代價生成樹的那條路徑。
key表示指定節點的編號;
value表示在最小代價生成樹中,該節點的前驅節點的編號。lowcost:
Map<String,Integer> lowcost;
記錄指定節點為終點的邊的最小權值。
key表示指定節點的編號;
value表示在最小代價生成樹中,以該節點為終點的邊的權值。k節點:
最新選入生成樹的節點。
演算法過程
第一步:
首先初始化陣列:
1. mark的值全為false
2. nearest的值全為-1
3. lowcost的值全為Integer.MAX_VALUE。
mark[1] | mark[2] | mark[3] | mark[4] | mark[5] | mark[6] |
---|---|---|---|---|---|
false | false | false | false | false | false |
lowcost[1] | lowcost[2] | lowcost[3] | lowcost[4] | lowcost[5] | lowcost[6] |
---|---|---|---|---|---|
MAX | MAX | MAX | MAX | MAX | MAX |
nearest[1] | nearest[2] | nearest[3] | nearest[4] | nearest[5] | nearest[6] |
---|---|---|---|---|---|
-1 | -1 | -1 | -1 | -1 | -1 |
第二步:
- 將節點1作為起點選入生成樹,記為k,mark[1]=true;
- 遍歷節點k的所有相鄰節點,更新lowcost陣列和nearest陣列:
設j是節點k相鄰節點,並且如果< k,j>這條邊的權值小於lowcost[j],則更新lowcost[j]=w< k,j>、nearest[j]=k。 - 在lowcost陣列中找到那個權值最小,且不在生成樹中的邊的節點,將它加入生成樹中:
3.1. 遍歷lowcost,找出最小值;
3.2. 將該最小值對應的lowcost下標(節點編號)的mark設為true;
3.3. 更新k;
mark[1] | mark[2] | mark[3] | mark[4] | mark[5] | mark[6] |
---|---|---|---|---|---|
true | false | false | false | false | false |
lowcost[1] | lowcost[2] | lowcost[3] | lowcost[4] | lowcost[5] | lowcost[6] |
---|---|---|---|---|---|
MAX | 6 | 1 | 5 | MAX | MAX |
nearest[1] | nearest[2] | nearest[3] | nearest[4] | nearest[5] | nearest[6] |
---|---|---|---|---|---|
-1 | 1 | 1 | 1 | 0 | 0 |
第三步:
- 此時將節點3記為k;
- 依次遍歷與k節點相鄰的所有不在生成樹中的節點,並更新nearest陣列和lowcost陣列;
- 遍歷lowcost陣列,找出尚未選中的最短的邊,將該邊的終點設為true,並設為k,一直迴圈下去,直到選出n-1條邊為止。
程式碼實現
/**
* prim演算法
* @param graph:圖的鄰接矩陣
*/
void prim(Map<String,List<Edge>> graph){
// 初始化
Map<String,String> nearest = new HashMap<>();
Map<String,Integer> lowcost = new HashMap<>();
Map<String,Boolean> mark = new HashMap<>();
String k = null;
String end = null;// 記錄最後一個節點的id,用於從後向前輸出結果
for( String id : graph.keySet() ){
nearest.put( id, null );
lowcost.put( id, Integer.MAX_VALUE );
mark.put( id, false );
k = id;
}
mark.put( id, true );
// 尋找生成樹的n-1條邊
for(int i=1; i<=graph.size()-1; i++){
// 更新與k相鄰的nearest
List<ENode> edges = graph.get( k );
for( ENode edge : edges ){
if ( !mark.get(edge.id) && edge.w < lowcost.get(edge.id) ) {
lowcost.put( edge.id, edge.w );
nearest.put( edge.id, k );
}
}
// 尋找當前lowcost中最短的邊
int min = Integer.MAX_VALUE;
for( Map.Entry<String,Integer> entry : lowcost.entrySet() ){
if ( entry.getValue() < min ) {
min = entry.getValue();
k = entry.getKey();
}
}
mark.get( k, true );
end = k;
}
// 輸出結果
for ( int i=0; i<graph.size(); i++ ) {
System.out.println( nearest.get(end)+"-"+end+"權值:"+lowcost.get(end) );
end = nearest.get(end);
}
}
時間複雜度
若圖中共有n個節點,那麼Prim演算法的時間複雜度為O(n^2)。
Kruskal演算法
貪心準則:將所有的邊按照權值遞增的順序排序,每次選一條權值最小的邊納入生成樹中,若沒有環路則選邊成功,若有環路,則選下一條次小的邊,直到選滿n-1條邊為止。