Kruskal演算法

weixin_30488085發表於2020-04-06

--------------------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; //邊的兩個頂點ab(如果是有向圖,就預設從頂點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;

//二維矩陣:nn列,全部初始化為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】

轉載於:https://www.cnblogs.com/siwuxie095/p/7124300.html

相關文章