最小生成樹——Prim演算法和Kruscal演算法

聽聞__發表於2018-04-29

1、最小生成樹的定義

        一個連通圖的生成樹是該連通圖的一個極小連通子圖,它含有圖中全部頂點,但只構成一棵樹的(n-1)條邊。對於一個帶權連通無向圖G的不同生成樹,各棵樹的邊上的權值之和可能不同,邊上的權值之和最小的樹稱為該圖的最小生成樹。

2、最小生成樹的應用

        比如要在n個城市之間鋪設光纜,主要目標是要使這 n 個城市的任意兩個之間都可以通訊,但鋪設光纜的費用很高,且各個城市之間鋪設光纜的費用不同,因此另一個目標是要使鋪設光纜的總費用最低。這就需要找到帶權的最小生成樹。

3、最小生成樹的構造演算法

        圖的鄰接矩陣型別定義如下:

#define N 100

typedef char ElemType;

//adjacency matrix graph
typedef struct MGraph
{
	ElemType vertexes[N];
	int edges[N][N];
	int n;
}MGraph;

(1)普里姆(Prim)演算法

        Prim演算法的核心思想是,從一個頂點開始,加入頂點集合,將頂點集合中所有頂點到其他頂點的邊作為候選邊,每次從候選邊中挑選最小權值的邊作為生成樹的邊,然後更新候選邊,直到圖的所有頂點都被加入為止。其步驟如下:

        ①初始化頂點集合為傳入的那個頂點,初始化候選邊為該頂點到其他頂點的所有邊

        ②重複步驟③,直到圖中所有頂點都被加入為止

        ③從候選邊中挑選最小權值的邊作為生成樹的邊,此時因為加入了新的頂點,所以需要更新候選邊

        程式碼實現:

void Prim(MGraph& g, int k)
{
	int* w = new int[g.n]; //the least edge weight
	int* v = new int[g.n]; //vertexes of the least weight
	for (int i = 0; i < g.n; i++)
	{
		w[i] = g.edges[k][i];
		v[i] = k;
	}
	w[k] = 0;
	for (int cnt = 1; cnt < g.n; cnt++)
	{
		//find the min val of w
		int imin = -1;
		for (int i = 0; i < g.n; i++)
		{
			if (w[i] != 0 && (imin == -1 || w[i] < w[imin]))
				imin = i;
		}
		//print
		cout << "(" << g.vertexes[v[imin]] << "," << g.vertexes[imin] << ")";
		//update the least weight
		w[imin] = 0; 
		for (int i = 0; i < g.n; i++)
		{
			if (g.edges[imin][i] != 0 && g.edges[imin][i] < w[i])
			{
				w[i] = g.edges[imin][i];
				v[i] = imin;
			}
		}
	}
	delete[] w, v;
}

        Prim演算法包含兩重for迴圈,其時間複雜度為O(n²)。

(2)克魯斯卡爾(Kruscal)演算法

        Kruscal演算法的基本思想是,將圖的所有邊按權值遞增的順序進行排序,然後每次依次從小到大選擇一條邊加入到生成樹中,但加入的前提是加入這條邊後不會構成迴路。其步驟如下:

        ①將圖的所有邊按權值遞增的順序進行排序

        ②重複步驟③,直到圖中所有頂點都被加入為止

        ③按照從小到大的順序選擇一條邊,如果這條邊未使生成樹形成迴路,就把這條邊加入到生成樹中

        程式碼實現如下:

void Kruscal(MGraph& g)
{
	//define the Edge
	typedef struct Edge
	{
		int u, v;
		int w;
		Edge(int _u, int _v, int _w) :u(_u), v(_v), w(_w) {}
	}Edge;
	//init the edges
	vector<Edge> v;
	for (int i = 0; i < g.n; i++)
	{
		for (int j = 0; j < g.n; j++)
		{
			auto w = g.edges[i][j];
			if (w != INT16_MAX && w != 0)
				v.push_back(Edge(i, j, w));
		}
	}
	//sort by weight increment
	sort(v.begin(), v.end(), [](Edge u, Edge v)->int {
		return u.w < v.w;
	});
	//add all edges if it does not constitute a loop
	int* set = new int[g.n];
	for (int i = 0; i < g.n; i++)
		set[i] = i;
	vector<Edge>::iterator iter = v.begin();
	for (int cnt = 0; cnt < g.n - 1; )
	{
		if (set[iter->u] != set[iter->v])
		{
			cout << "(" << g.vertexes[iter->u] << "," << g.vertexes[iter->v] << ")";
			cnt++;
			for (int i = 0; i < g.n; i++)
			{
				if (set[i] == set[iter->v])
					set[i] = set[iter->u];
			}
		}
		iter++;
	}
	delete[] set;
}

        Kruscal演算法同樣包含兩重for迴圈,其時間複雜度為O(n²),但可以對Kruscal演算法做兩方面的優化,一是將邊集排序改為堆排序,二是用並查集來判斷新加入邊是否構成迴路,優化後Kruscal演算法的時間複雜度為O(elog₂e),e為圖的所有邊。

4、測試

求出給定無向帶權圖的最小生成樹。圖的定點為字元型,權值為不超過100的整形。

輸入

第一行為圖的頂點個數n
第二行為圖的邊的條數e
接著e行為依附於一條邊的兩個頂點和邊上的權值

輸出

最小生成樹中的邊。

樣例輸入

6
10
ABCDEF
A B 6
A C 1
A D 5
B C 5
C D 5
B E 3
E C 6
C F 4
F D 2
E F 6

樣例輸出

(A,C)(C,F)(F,D)(C,B)(B,E)
(A,C)(D,F)(B,E)(C,F)(B,C)

程式碼如下:

#include <iostream>
#include <map>
#include <vector>
#include <algorithm>
using namespace std;

#define N 100

typedef char ElemType;

//adjacency matrix graph
typedef struct MGraph
{
	ElemType vertexes[N];
	int edges[N][N];
	int n;
}MGraph;

void Prim(MGraph& g, int k)
{
	int* w = new int[g.n]; //the least edge weight
	int* v = new int[g.n]; //vertexes of the least weight
	for (int i = 0; i < g.n; i++)
	{
		w[i] = g.edges[k][i];
		v[i] = k;
	}
	w[k] = 0;
	for (int cnt = 1; cnt < g.n; cnt++)
	{
		//find the min val of w
		int imin = -1;
		for (int i = 0; i < g.n; i++)
		{
			if (w[i] != 0 && (imin == -1 || w[i] < w[imin]))
				imin = i;
		}
		//print
		cout << "(" << g.vertexes[v[imin]] << "," << g.vertexes[imin] << ")";
		//update the least weight
		w[imin] = 0; 
		for (int i = 0; i < g.n; i++)
		{
			if (g.edges[imin][i] != 0 && g.edges[imin][i] < w[i])
			{
				w[i] = g.edges[imin][i];
				v[i] = imin;
			}
		}
	}
	delete[] w, v;
}

void Kruscal(MGraph& g)
{
	//define the Edge
	typedef struct Edge
	{
		int u, v;
		int w;
		Edge(int _u, int _v, int _w) :u(_u), v(_v), w(_w) {}
	}Edge;
	//init the edges
	vector<Edge> v;
	for (int i = 0; i < g.n; i++)
	{
		for (int j = 0; j < g.n; j++)
		{
			auto w = g.edges[i][j];
			if (w != INT16_MAX && w != 0)
				v.push_back(Edge(i, j, w));
		}
	}
	//sort by weight increment
	sort(v.begin(), v.end(), [](Edge u, Edge v)->int {
		return u.w < v.w;
	});
	//add all edges if it does not constitute a loop
	int* set = new int[g.n];
	for (int i = 0; i < g.n; i++)
		set[i] = i;
	vector<Edge>::iterator iter = v.begin();
	for (int cnt = 0; cnt < g.n - 1; )
	{
		if (set[iter->u] != set[iter->v])
		{
			cout << "(" << g.vertexes[iter->u] << "," << g.vertexes[iter->v] << ")";
			cnt++;
			for (int i = 0; i < g.n; i++)
			{
				if (set[i] == set[iter->v])
					set[i] = set[iter->u];
			}
		}
		iter++;
	}
	delete[] set;
}

int main()
{
	MGraph g;
	while(cin >> g.n)
	{
		//input
		int e;
		cin >> e;
		map<char, int> m;
		for (int i = 0; i < g.n; i++)
		{
			cin >> g.vertexes[i];
			m[g.vertexes[i]] = i;
		}
		for (int i = 0; i < g.n; i++)
			for (int j = 0; j < g.n; j++)
				g.edges[i][j] = INT16_MAX;
		
		char u, v;
		int w;
		for (int cnt = 0; cnt < e; cnt++)
		{
			cin >> u >> v >> w;
			g.edges[m[u]][m[v]] = g.edges[m[v]][m[u]] = w;
		}
		//excute
		Prim(g, 0); cout << endl; //Prim algorithm
		Kruscal(g); cout << endl; //Kruscal algorithm
	}
	return 0;
}

參考文獻

[1] 李春葆.資料結構教程.清華大學出版社,2013.

[2] 最小生成樹.百度百科[引用日期2018-04-29].

相關文章