--------------------siwuxie095
Kruskal 演算法
在 Prim 演算法中,不停地改變切分,同時通過切分尋找
橫切邊中權值最小的那條邊
在這個過程中,可能有人就會有這樣一個想法:如果每
次都找當前權值最小的那條邊(不是橫切邊中),那麼
它就一定屬於最小生成樹
看如下例項:
這張連通帶權無向圖中所有邊上的權值如下:
1-7 是權值最小的邊,權值為 0.16,就可以說 1-7 一定
屬於最小生成樹
這是因為:總能找到一個切分,使得對於這個切分而言,
1-7 就是橫切邊中的權值最小的那條邊
採用這樣的思路不斷去找當前權值最小的邊,只要這些
權值最小的邊不構成環,那麼這些依次取得的邊就一定
屬於最小生成樹,這就是 Kruskal 演算法的思想
具體做法:
首先將圖中所有的邊進行一次排序,時間複雜度:O(E*lgE)
然後每次都取出還未考慮的邊中的權值最小的那條邊,把它
加入到最小生成樹中,看看是否會形成環,如果不會形成環,
那麼它就一定屬於最小生成樹
整個過程中比較複雜的,唯一需要處理的就是:怎麼判斷把
一個邊加入到最小生成樹中是否會形成環
其實,這個判斷的方式也非常簡單,只需要將並查集作為輔
助資料結構,就可以很容易地判斷出來
即 在將一條邊加入到最小生成樹的同時,只要對這條邊的兩
個端點要進行一次 Union 操作,後續再加入某一條邊時就可
以使用並查集快速判斷環
程式:
Edge.h:
#ifndef EDGE_H #define EDGE_H
#include <iostream> #include <cassert> using namespace std;
//邊資訊:兩個頂點和權值 template<typename Weight> class Edge {
private:
int a, b; //邊的兩個頂點a和b(如果是有向圖,就預設從頂點a指向頂點b) Weight weight; //邊上的權值
public:
Edge(int a, int b, Weight weight) { this->a = a; this->b = b; this->weight = weight; }
//預設建構函式 Edge(){}
~Edge(){}
int v(){ return a; }
int w(){ return b; }
Weight wt() { return weight; }
//知道邊的一個頂點x,返回另一個頂點 int other(int x) { assert(x == a || x == b); return x == a ? b : a; }
//友元函式過載 friend ostream &operator<<(ostream &os, const Edge &e) { os << e.a << "-" << e.b << ": " << e.weight; return os; }
bool operator<(Edge<Weight> &e) { return weight < e.wt(); }
bool operator<=(Edge<Weight> &e) { return weight <= e.wt(); }
bool operator>(Edge<Weight> &e) { return weight > e.wt(); }
bool operator>=(Edge<Weight> &e) { return weight >= e.wt(); }
bool operator==(Edge<Weight> &e) { return weight == e.wt(); } };
#endif |
SparseGraph.h:
#ifndef SPARSEGRAPH_H #define SPARSEGRAPH_H
#include "Edge.h" #include <iostream> #include <vector> #include <cassert> using namespace std;
// 稀疏圖 - 鄰接表 template<typename Weight> class SparseGraph {
private:
int n, m; //n 和 m 分別表示頂點數和邊數 bool directed; //directed表示是有向圖還是無向圖 vector<vector<Edge<Weight> *>> g; //g[i]裡儲存的就是和頂點i相鄰的所有邊指標
public:
SparseGraph(int n, bool directed) { this->n = n; this->m = 0; this->directed = directed; //g[i]初始化為空的vector for (int i = 0; i < n; i++) { g.push_back(vector<Edge<Weight> *>()); } }
~SparseGraph() {
for (int i = 0; i < n; i++) { for (int j = 0; j < g[i].size(); j++) { delete g[i][j]; } } }
int V(){ return n; } int E(){ return m; }
void addEdge(int v, int w, Weight weight) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
g[v].push_back(new Edge<Weight>(v, w, weight)); //(1)頂點v不等於頂點w,即 不是自環邊 //(2)且不是有向圖,即 是無向圖 if (v != w && !directed) { g[w].push_back(new Edge<Weight>(w, v, weight)); }
m++; }
//hasEdge()判斷頂點v和頂點w之間是否有邊 //hasEdge()的時間複雜度:O(n) bool hasEdge(int v, int w) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
for (int i = 0; i < g[v].size(); i++) { if (g[v][i]->other(v) == w) { return true; } }
return false; }
void show() {
for (int i = 0; i < n; i++) { cout << "vertex " << i << ":\t"; for (int j = 0; j < g[i].size(); j++) { cout << "{to:" << g[i][j]->w() << ",wt:" << g[i][j]->wt() << "}\t"; } cout << endl; } }
//鄰邊迭代器(相鄰,即 adjacent) // //使用迭代器可以隱藏迭代的過程,按照一定的 //順序訪問一個容器中的所有元素 class adjIterator { private:
SparseGraph &G; //圖的引用,即 要迭代的圖 int v; //頂點v int index; //相鄰頂點的索引
public:
adjIterator(SparseGraph &graph, int v) : G(graph) { this->v = v; this->index = 0; }
//要迭代的第一個元素 Edge<Weight> *begin() { //因為有可能多次呼叫begin(), //所以顯式的將index設定為0 index = 0; //如果g[v]的size()不為0 if (G.g[v].size()) { return G.g[v][index]; }
return NULL; }
//要迭代的下一個元素 Edge<Weight> *next() { index++; if (index < G.g[v].size()) { return G.g[v][index]; }
return NULL; }
//判斷迭代是否終止 bool end() { return index >= G.g[v].size(); } }; };
#endif |
DenseGraph.h:
#ifndef DENSEGRAPH_H #define DENSEGRAPH_H
#include "Edge.h" #include <iostream> #include <vector> #include <cassert> using namespace std;
// 稠密圖 - 鄰接矩陣 template<typename Weight> class DenseGraph {
private:
int n, m; //n 和 m 分別表示頂點數和邊數 bool directed; //directed表示是有向圖還是無向圖 vector<vector<Edge<Weight> *>> g; //二維矩陣,儲存邊指標
public:
DenseGraph(int n, bool directed) { this->n = n; this->m = 0; this->directed = directed; //二維矩陣:n行n列,全部初始化為NULL for (int i = 0; i < n; i++) { g.push_back(vector<Edge<Weight> *>(n, NULL)); } }
~DenseGraph() { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (g[i][j] != NULL) { delete g[i][j]; } } } }
int V(){ return n; } int E(){ return m; }
//在頂點v和頂點w之間建立一條邊 void addEdge(int v, int w, Weight weight) { assert(v >= 0 && v < n); assert(w >= 0 && w < n);
//如果頂點v和頂點w之間已經存在一條邊,就刪掉, //之後按照傳入權值重建一條邊,即直接覆蓋 if (hasEdge(v, w)) { delete g[v][w];
//如果是無向圖,還要刪除和主對角線對稱的值 if (!directed) { delete g[w][v]; }
m--; }
g[v][w] = new Edge<Weight>(v, w, weight);
//如果是無向圖,還要在和主對角線對稱處新增值 if (!directed) { g[w][v] = new Edge<Weight>(w, v, weight); }
m++; }
//hasEdge()判斷頂點v和頂點w之間是否有邊 //hasEdge()的時間複雜度:O(1) bool hasEdge(int v, int w) { assert(v >= 0 && v < n); assert(w >= 0 && w < n); return g[v][w] != NULL; }
void show() {
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (g[i][j]) { cout << g[i][j]->wt() << "\t"; } else { cout << "NULL\t"; } } cout << endl; } }
//鄰邊迭代器(相鄰,即 adjacent) class adjIterator { private:
DenseGraph &G; //圖引用,即 要迭代的圖 int v; //頂點v int index; //相鄰頂點的索引
public:
adjIterator(DenseGraph &graph, int v) : G(graph) { this->v = v; this->index = -1; }
//要迭代的第一個元素 Edge<Weight> *begin() { //找第一個權值不為NULL的元素,即為要迭代的第一個元素 index = -1; return next(); }
//要迭代的下一個元素 Edge<Weight> *next() { for (index += 1; index < G.V(); index++) { if (G.g[v][index]) { return index; } }
return NULL; }
//判斷迭代是否終止 bool end() { return index >= G.V(); } }; };
#endif |
ReadGraph.h:
#ifndef READGRAPH_H #define READGRAPH_H
#include <iostream> #include <string> #include <fstream> #include <sstream> #include <cassert> using namespace std;
//從檔案中讀取圖的測試用例 template <typename Graph, typename Weight> class ReadGraph {
public: ReadGraph(Graph &graph, const string &filename) {
ifstream file(filename); string line; //一行一行的讀取 int V, E;
assert(file.is_open());
//讀取file中的第一行到line中 assert(getline(file, line)); //將字串line放在stringstream中 stringstream ss(line); //通過stringstream解析出整型變數:頂點數和邊數 ss >> V >> E;
//確保檔案裡的頂點數和圖的建構函式中傳入的頂點數一致 assert(V == graph.V());
//讀取file中的其它行 for (int i = 0; i < E; i++) {
assert(getline(file, line)); stringstream ss(line);
int a, b; Weight w; ss >> a >> b >> w; assert(a >= 0 && a < V); assert(b >= 0 && b < V); graph.addEdge(a, b, w); } } };
#endif |
MinHeap.h:
#ifndef MINHEAP_H #define MINHEAP_H
#include <iostream> #include <algorithm> #include <string> #include <cmath> #include <cassert> using namespace std;
//最小堆:索引從0開始 template<typename Item> class MinHeap {
private: Item *data; int count; int capacity;
//私有函式,使用者不能呼叫 void shiftUp(int k) { //如果新新增的元素小於父節點的元素,則進行交換 while (k > 0 && data[(k - 1) / 2] > data[k]) { swap(data[(k - 1) / 2], data[k]); k = (k - 1) / 2; } }
//也是私有函式,使用者不能呼叫 void shiftDown(int k) { //只要當前節點有孩子就進行迴圈 while (2 * k + 1 < count) { // 在此輪迴圈中,data[k]和data[j]交換位置 int j = 2 * k + 1;
// data[j]是data[2*k]和data[2*k+1]中的最小值 if (j + 1 < count && data[j + 1] < data[j]) { j++; }
if (data[k] <= data[j]) { break; }
swap(data[k], data[j]); k = j; } }
public:
MinHeap(int capacity) { data = new Item[capacity]; //計數器,即 序列號,這裡索引等於序列號減一 count = 0; this->capacity = capacity; }
~MinHeap() { delete []data; }
int size() { return count; }
bool isEmpty() { return count == 0; }
//向最小堆中新增新元素,新元素放在陣列末尾 void insert(Item item) { //防止越界 assert(count <= capacity);
//索引從0開始 data[count] = item; count++;
//新加入的元素有可能破壞最小堆的定義,需要通過 //Shift Up操作,把索引為count-1的元素嘗試著向上 //移動來保持最小堆的定義 shiftUp(count - 1); }
//取出最小堆中根節點的元素(最小值) Item extractMin() { //首先要保證堆不為空 assert(count > 0);
//取出根節點的元素(最小值) Item ret = data[0];
//將第一個元素(最小值)和最後一個元素進行交換 swap(data[0], data[count - 1]);
//count--後,被取出的根節點就不用再考慮了 count--;
//呼叫Shift Down操作,想辦法將此時的根節點(索引為0) //向下移動,來保持最小堆的定義 shiftDown(0);
return ret; }
public:
//在控制檯列印測試用例 void testPrint() {
//限制:只能列印100個元素以內的堆,因為控制檯一行的字元數量有限 if (size() >= 100) { cout << "Fancy print can only work for less than 100 int"; return; }
//限制:只能列印型別是int的堆 if (typeid(Item) != typeid(int)) { cout << "Fancy print can only work for int item"; return; }
cout << "The Heap size is: " << size() << endl; cout << "data in heap: "; for (int i = 0; i < size(); i++) { cout << data[i] << " "; } cout << endl; cout << endl;
int n = size(); int max_level = 0; int number_per_level = 1; while (n > 0) { max_level += 1; n -= number_per_level; number_per_level *= 2; }
int max_level_number = int(pow(2, max_level - 1)); int cur_tree_max_level_number = max_level_number; int index = 0; for (int level = 0; level < max_level; level++) { string line1 = string(max_level_number * 3 - 1, ' ');
int cur_level_number = min(count - int(pow(2, level)) + 1, int(pow(2, level)));
bool isLeft = true;
for (int index_cur_level = 0; index_cur_level < cur_level_number; index++, index_cur_level++) { putNumberInLine(data[index], line1, index_cur_level, cur_tree_max_level_number * 3 - 1, isLeft);
isLeft = !isLeft; } cout << line1 << endl;
if (level == max_level - 1) { break; }
string line2 = string(max_level_number * 3 - 1, ' '); for (int index_cur_level = 0; index_cur_level < cur_level_number; index_cur_level++) { putBranchInLine(line2, index_cur_level, cur_tree_max_level_number * 3 - 1); }
cout << line2 << endl;
cur_tree_max_level_number /= 2; } }
private:
void putNumberInLine(int num, string &line, int index_cur_level, int cur_tree_width, bool isLeft) {
int sub_tree_width = (cur_tree_width - 1) / 2;
int offset = index_cur_level * (cur_tree_width + 1) + sub_tree_width;
assert(offset + 1 < line.size());
if (num >= 10) { line[offset + 0] = '0' + num / 10; line[offset + 1] = '0' + num % 10; } else { if (isLeft) line[offset + 0] = '0' + num; else line[offset + 1] = '0' + num; } }
void putBranchInLine(string &line, int index_cur_level, int cur_tree_width) {
int sub_tree_width = (cur_tree_width - 1) / 2;
int sub_sub_tree_width = (sub_tree_width - 1) / 2;
int offset_left = index_cur_level * (cur_tree_width + 1) + sub_sub_tree_width;
assert(offset_left + 1 < line.size());
int offset_right = index_cur_level * (cur_tree_width + 1) + sub_tree_width + 1 + sub_sub_tree_width;
assert(offset_right < line.size());
line[offset_left + 1] = '/'; line[offset_right + 0] = '\\'; } };
#endif |
UnionFind.h:
#ifndef UNIONFIND_H #define UNIONFIND_H
#include <iostream> #include <cassert> using namespace std;
//並查集:Quick Union + rank + path compression class UnionFind {
private: int* parent; int* rank; // rank[i]表示以i為根的集合所表示的樹的層數 int count;
public: UnionFind(int count) { this->count = count; parent = new int[count]; rank = new int[count]; //在初始情況下,並查集裡的元素,兩兩之間互不連線 for (int i = 0; i < count; i++) { parent[i] = i; rank[i] = 1; } }
~UnionFind() { delete []parent; delete []rank; }
int size() { return count; }
int find(int p) {
assert(p >= 0 && p < count);
// path compression 1 while (p != parent[p]) { //路徑壓縮 parent[p] = parent[parent[p]]; p = parent[p]; }
return p; }
bool isConnected(int p, int q) { return find(p) == find(q); }
void unionElements(int p, int q) {
int pRoot = find(p); int qRoot = find(q);
if (pRoot == qRoot) { return; }
//rank小的那棵樹的根節點指向rank大的那棵樹的根節點 if (rank[pRoot] < rank[qRoot]) { parent[pRoot] = qRoot; } else if (rank[qRoot] < rank[pRoot]) { parent[qRoot] = pRoot; } // rank[pRoot] == rank[qRoot] else { //可互換 parent[pRoot] = qRoot; rank[qRoot] ++; }
}
void show() { for (int i = 0; i < count; i++) { cout << i << " : " << parent[i] << endl; } } };
//路徑壓縮:在尋找根的時候,兩步一跳,比原來的 Find 操作要快, //與此同時,如果下一次要尋找這棵樹上某個元素的根節點,由於層 //數變低,相應的速度也會快很多
#endif |
KruskalMST.h:
#ifndef KRUSKALMST_H #define KRUSKALMST_H
#include "Edge.h" #include "MinHeap.h" #include "UnionFind.h" #include <iostream> #include <vector> using namespace std;
//Kruskal 演算法實現最小生成樹 template <typename Graph, typename Weight> class KruskalMST {
private:
vector<Edge<Weight>> mst; //屬於最小生成樹的 V-1 條邊儲存到向量 mst 中 Weight mstWeight; //最後最小生成樹的總權值 mstWeight
public:
KruskalMST(Graph &graph) { //使用堆排序(最小堆) MinHeap<Edge<Weight>> pq(graph.E()); //遍歷圖中所有的邊 for (int i = 0; i < graph.V(); i++) { //注意:宣告迭代器時,前面還要加 typename,表明 adjIterator //是 Graph 中的型別,而不是成員變數 typename Graph::adjIterator adj(graph, i); for (Edge<Weight> *e = adj.begin(); !adj.end(); e = adj.next()) { //對邊 e 兩端的頂點索引進行比較,只將一端索引更小的邊 //放入最小堆中,避免重複 if (e->v() < e->w()) { pq.insert(*e); } } }
UnionFind uf = UnionFind(graph.V()); //只要最小堆不為空, while (!pq.isEmpty() && mst.size() < graph.V() - 1) {
Edge<Weight> e = pq.extractMin(); //如果邊 e 兩端的頂點索引有相同的根,即 相連, //那麼就不考慮邊 e,直接跳過 if (uf.isConnected(e.v(), e.w())) { continue; }
mst.push_back(e);
uf.unionElements(e.v(), e.w()); }
mstWeight = mst[0].wt(); for (int i = 1; i < mst.size(); i++) { mstWeight += mst[i].wt(); }
}
~KruskalMST() {
}
vector<Edge<Weight>> mstEdges() { return mst; }
Weight result() { return mstWeight; } };
#endif |
main.cpp:
#include "SparseGraph.h" #include "DenseGraph.h" #include "ReadGraph.h" #include "KruskalMST.h" #include <iostream> #include <iomanip> using namespace std;
int main() {
string filename = "testG1.txt"; int V = 8;
//稀疏圖 SparseGraph<double> g = SparseGraph<double>(V, false); ReadGraph<SparseGraph<double>, double> readGraph(g, filename);
// Test Kruskal MST cout << "Test Kruskal MST:" << endl; KruskalMST<SparseGraph<double>, double> kruskalMST(g); vector<Edge<double>> mst = kruskalMST.mstEdges(); for (int i = 0; i < mst.size(); i++) { cout << mst[i] << endl; } cout << "The MST weight is: " << kruskalMST.result() << endl;
system("pause"); return 0; }
//Kruskal 演算法的時間複雜度:O(E*logE+E*logV),比 Prim 演算法的效率要低 |
執行一覽:
其中,testG1.txt 的內容如下:
該檔案可以分成兩個部分:
(1)第一行:兩個數字分別代表頂點數和邊數
(2)其它行:每一行的前兩個數字表示一條邊,第三個數字表示權值
【made by siwuxie095】