最小生成樹是指包含圖中所有的頂點而又沒有環並且所有邊的權值最小的子圖,由於這張圖沒有環,所以就是一棵樹。比較流行的兩種找到最小生成樹的算法有Kruscal算法和Prim算法。本文在代碼註釋裏寫明算法的原理和實際計算步驟,然後貼出兩種算法運行的結果示例,最後證明算法的正確性。
原理及實現
#ifndef _MINGENTREE_H_
#define _MINGENTREE_H_
#include "../include/DirectedWeightGraph.h"
#include "../include/Vector.h"
#include "../include/UnionFindSet.h"
#include "../include/Heap.h"
using namespace MyDataStructure;
namespace MyTools
{
template <typename ValueType,typename WeightType>
class MinGenTree
{
public:
typedef typename DirectedWeightGraph<ValueType, WeightType> GraphType;
typedef typename GraphType::VerticePtr VerticePtr;
typedef typename GraphType::EdgePtr EdgePtr;
public:
Vector<EdgePtr> Kruscal(const GraphType &Graph)
{
int size = Graph.GetVerticeSize();
//算法的核心是,每一步選擇一條具有最小
//權值並且加入最小生成樹後不會產生迴路
//的邊,可以利用一個最小堆動態排序邊
Heap<EdgePtr, less> minHeap;
for (int i = 0; i < size; ++i)
{
VerticePtr v = Graph.GetVertice(i);
if (v != nullptr)
{
EdgePtr e = v->adj;
while (e != nullptr)
{
minHeap.Insert(e);
e = e->next;
}
}
}
//利用並查集判斷邊一條候選邊加入後
//是否會產生迴路
UnionFindSet ufs(size);
Vector<EdgePtr> minTree;
int VerticeCount = Graph.GetVerticeCount();
while (minHeap.Size() != 0 && minTree.Size() < VerticeCount)
{
EdgePtr e;
if (minHeap.GetTop(e))
{
minHeap.RemoveTop();
//如果當前最小一條邊的兩個頂點不同時在最小生成樹
//集合或剩餘的圖裏,那麼這條邊就可以加入最小生成樹,
//就不會產生迴路
if (ufs.Find(e->src) != ufs.Find(e->dst))
{
minTree.PushBack(e);
ufs.Union(e->src, e->dst);
}
}
}
//如果最小生成樹的邊的數目不是頂點數減一,那這棵樹就不是一棵最小生成樹
if (minTree.Size() < VerticeCount - 1) return Vector<EdgePtr>();
else return minTree;
}
Vector<EdgePtr> Prim(const GraphType Graph)
{
int size = Graph.GetVerticeSize();
//算法的核心是每次從跨越最小生成樹集合與
//剩餘圖的集合的邊中選擇具有最小權值的一條邊
//加入生成樹,所以可以利用最小堆動態排序跨越
//兩個集合的邊
Heap<EdgePtr, less> minHeap;
Vector<EdgePtr> minTreeEdge;
//只有一條邊的一個頂點在生成樹中,而另一個頂點
//不在,纔有可能被選擇
Vector<bool> IsInMST(size);
for (int i = 0; i < size; ++i)
{
IsInMST[i] = false;
}
//選擇一個頂點開始
Vector<int> minTreeVertice;
for (int i = 0; i < size; ++i)
{
if (Graph.GetVertice(i) != nullptr)
{
minTreeVertice.PushBack(i);
IsInMST[i] = true;
break;
}
}
int VerticeCount = Graph.GetVerticeCount();
do
{
int mst_v = minTreeVertice.Size();
EdgePtr e = Graph.GetVertice(minTreeVertice[mst_v - 1])->adj;
//找到新加入的頂點帶來的跨越兩個集合的邊,加入最小堆,作爲候選點
while (e != nullptr)
{
if (IsInMST[e->dst] == false) minHeap.Insert(e);
e = e->next;
}
EdgePtr top = nullptr;
//由於加入了一個新的節點,所以以前的一些候選邊由於兩個
//頂點都在最小生成樹集合裏而不能再被選擇,但它們又可能
//處於最小堆的頂端,把這種邊移除
while (minHeap.GetTop(top) == true && IsInMST[top->dst] == true)
{
minHeap.RemoveTop();
top = nullptr;
}
//除非堆中還有可用的邊,否則就退出循環了
if (top != nullptr)
{
if (IsInMST[top->dst] = true)
{
minHeap.RemoveTop();
minTreeVertice.PushBack(top->dst);
minTreeEdge.PushBack(top);
IsInMST[top->dst] = true;
}
}
else
{
break;
}
} while (minTreeVertice.Size() < VerticeCount);
//如果最小生成樹的邊的數目不是頂點數減一,那這棵樹就不是一棵最小生成樹
if (minTreeEdge.Size() < VerticeCount - 1) return Vector<EdgePtr>();
else return minTreeEdge;
}
private:
struct less
{
bool operator ()(const EdgePtr op1, const EdgePtr op2)
{
return op1->weight < op2->weight;
}
};
};
}
#endif
算法運行結果示例
原圖
Kruscal算法運行結果
Prim算法運行結果
可以看到兩種算法運行的結果一樣,並且也是正確的。
算法正確性的證明
Kruscal算法:
假設還有另一條邊E’比當前選擇的邊更合適,那麼將它加入最小生成樹就必然要刪除一條邊E,否則就會形成環。但是如果E’替換E後使得最小生成樹的總權值更小,那麼就說明E’的權值小於E,那麼算法運行過程中就該選擇E’,而這與算法運行過程矛盾,所以這樣的E’不存在,如果E’的權值等於E,那麼是可以替換的,因爲最小生成樹並不是唯一的。
Prim算法證明與之類似。