1. 程式人生 > >最小生成樹演算法-Prim演算法

最小生成樹演算法-Prim演算法

  1. 從任意一個頂點開始構造生成樹,假設從0號頂點開始。首先將頂點0加入到生成樹中,用一個一維陣列book標記哪些頂點已經加入到生成樹。
  2. 用一個數組dis記錄生成樹到各個頂點的距離。最初生成樹中只有0號頂點,當其餘頂點與0號頂點有直連邊時,陣列dis中儲存的就是0號頂點到該頂點的邊的權值。與0號頂點沒有直連邊的頂點對應的dis陣列中的元素設為無窮大。完成dis陣列初始化。
  3. 從陣列dis中選出離生成樹最近的頂點(假設這個頂點為j)加入到生成樹中(即在陣列dis中找未加入生成樹的點對應的dis元素最小值)。再以j為中間點,更新生成樹到每一個非樹頂點的距離(即鬆弛),即如果dis[k]>e[j][k] 則更新dis[k]=e[j][k]。(二維陣列e儲存的是圖中各個邊的資訊)。
  4. 重複第3步,直到生成樹中有n個頂點為止。
#include <iostream>
#include <vector>

using namespace std;

#define inf 99999

int main()
{
	int n, m;
	cin >> n >> m;//讀入n和m,n表示頂點個數,m表示邊的條數
	vector<vector<int>> e(n, vector<int>(n, inf));//儲存圖中個頂點間的距離資訊
	vector<bool> book(n,false);
	vector<int> dis(n);
	int count = 0, sum = 0;//count用來記錄生成樹中的頂點個數,sum用來儲存路徑之和

	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			if (i == j) {
				e[i][j] = 0;
			}
		}
	}

	//讀入邊
	for (int i = 0; i < m; i++) {
		int t1, t2, t3;
		cin >> t1 >> t2 >> t3;
		e[t1][t2] = t3;
		//注意這裡是無向圖,所以需要將邊反向再儲存一遍
		e[t2][t1] = t3;
	}

	//初始化dis陣列,當前生成樹中只有0號頂點,故儲存的是0號頂點到各個頂點的距離
	for (int i = 0; i < n; i++) {
		dis[i] = e[0][i];
	}

	//Prim演算法核心部分
	//將0號頂點加入生成樹
	book[0] = true;
	count++;
	while (count < n) {
		int min = inf;
		int j;
		for (int i = 0; i < n; i++) {
			if (book[i] == false && dis[i] < min) {
				min = dis[i];
				j = i;
			}
		}
		book[j] = true;
		count++;
		sum += dis[j];

		//掃描當前頂點j所有的邊,更新生成樹到每一個非樹頂點的距離
		for (int i = 0; i < n; i++) {
			if (book[i] == false && dis[i] > e[j][i]) {
				dis[i] = e[j][i];
			}
		}
	}

	cout << "sum=" << sum << endl;
	
	system("pause");
}

  這種方法的時間複雜度為O(N^2),如果藉助“堆”,每次選邊的時間複雜度是O(logM),然後使用鄰接表來儲存圖的話,整個演算法的時間複雜度會降低到O(MlogN)。

  使用堆來優化,我們需要3個數組。陣列dis用來記錄生成樹到各個頂點的距離。陣列h是一個最小堆,堆裡面儲存的是頂點編號。注意這裡不是按照頂點編號的大小來建立最小堆的,而是按照頂點在陣列dis中所對應的值來建立這個最小堆。此外還需要一個pos陣列來記錄每個頂點在最小堆h中的位置。

程式碼如下:

#include "pch.h"
#include <iostream>
#include <vector>

using namespace std;

#define inf 99999

//交換堆中兩個元素的值
void myswap(int x, int y, vector<int> &h, vector<int> &pos) {
	int temp;
	temp = h[x];
	h[x] = h[y];
	h[y] = temp;

	//同步更新pos陣列,pos陣列記錄的是每個頂點在堆中的位置
	temp = pos[h[x]];
	pos[h[x]] = pos[h[y]];
	pos[h[y]] = temp;
	return;
}

//最小堆向下調整函式  i是需要向下調整的節點編號  h是最小堆的底層實現
void siftdown(int i, vector<int> &h, const int &size,vector<int> &dis, vector<int> &pos ) {
	bool flag = false; //flag用來標記是否需要繼續向下調整
	int temp;
	while ((i + 1) * 2 - 1 < size && flag == false) {
		//比較i和它的左兒子(i+1)*2在dis中的值,並用temp記錄較小的節點編號
		if (dis[h[(i + 1) * 2 - 1]] < dis[h[i]]) {
			temp = (i + 1) * 2 - 1;
		}
		else {
			temp = i;
		}

		//如果i有右兒子,則繼續比較
		if ((i + 1) * 2 < size) {
			if (dis[h[(i + 1) * 2]] < dis[h[temp]]) {
				temp = (i + 1) * 2;
			}
		}

		if (temp != i) {
			myswap(temp, i, h, pos);
			i = temp;//更新i為剛與它交換的兒子節點的編號,便於接下來繼續向下調整
		}
		else {
			flag = true;
		}
	}
	return;
}

void siftup(int i, vector<int> &h, const int &size, vector<int> &dis, vector<int> &pos) {
	bool flag = false; //用來標記是否需要向上調整
	if (i == 0) {
		return; //如果是堆頂,就直接返回,不需要向上調整了
	}

	//不在堆頂,並且i的值比父節點小的時候繼續向上調整
	while (i != 0 && flag == false) {
		if (dis[h[i]] < dis[h[(i - 1) / 2]]) {
			myswap(i, (i - 1) / 2, h, pos);
		}
		else {
			flag = true;
		}
		i = (i - 1) / 2;
	}
	return;
}

int pop(vector<int> &h, int &size, vector<int> &dis, vector<int> &pos)
{
	int t = h[0]; //用一個臨時變數記錄堆頂點的編號
	pos[t] = -1;//彈出堆頂後,這個頂點已不在堆中了,故將該頂點對應的pos陣列中的值做相應處理
	h[0] = h[size - 1];//將堆的最後一個點賦值到堆頂
	pos[h[0]] = 0;
	size--;
	siftdown(0,h,size,dis,pos);
	return t;
}



int main()
{
	int n, m;
	cin >> n >> m;//讀入n和m,n表示頂點個數,m表示邊的條數
	vector<int> dis(n, inf);//記錄生成樹到各個頂點的距離
	vector<bool> book(n, false);//記錄那些頂點已經在生成樹中了,true表示已經在生成樹中了
	vector<int> h(n), pos(n);//h用來儲存最小堆,pos用來儲存各個頂點在最小堆中的位置
	int size;//size為最小堆的大小

	//u陣列儲存邊的起點,v陣列儲存邊的終點,w陣列儲存邊的權值
	//first[i]表示以點i為起點的第一條邊的編號
	//next[i]表示編號為i的邊的下一條邊的編號
	//因為儲存的是無向圖,所以u、v、w、next陣列的大小是邊的條數的兩倍
	vector<int> u(2*m), v(2*m), w(2*m),first(n,-1),next(2*m);
	//count儲存當前生成樹中頂點的個數,sum儲存當前路徑之和
	int count = 0, sum = 0;

	//開始讀入邊
	for (int i = 0; i < m; i++) {
		cin >> u[i] >> v[i] >> w[i];
	}

	//由於是無向圖,故需把所有的邊再反向儲存一次
	for (int i = m; i < 2 * m; i++) {
		u[i] = v[i - m];
		v[i] = u[i - m];
		w[i] = w[i - m];
	}

	//初始化鄰接表
	for (int i = 0; i < 2 * m; i++) {
		next[i] = first[u[i]];
		first[u[i]] = i;
	}

	//Prim演算法核心部分開始
	//將0號頂點加入生成樹
	book[0] = true;
	count++;

	//初始化dis陣列為0號頂點到其餘各個頂點的初始距離
	dis[0] = 0;
	int k = first[0];
	while (k != -1) {
		dis[v[k]] = w[k];
		k = next[k];
	}

	//初始化最小堆
	size = n;
	for (int i = 0; i < size; i++) {
		h[i] = i;
		pos[i] = i;
	}
	//因為堆中編號從0開始,所以i=size/2-1;如果是從1開始編號,那麼就是i=size/2;
	//只需要從倒數第二層從右向左第一個非葉節點開始向下調整即可
	for (int i = size / 2 - 1; i >= 0; i--) {
		siftdown(i, h, size, dis, pos);
	}
	pop(h,size,dis,pos);//從堆頂彈出一個元素,因為此時堆頂是0號頂點

	while (count < n) {
		int j = pop(h, size, dis, pos);
		book[j] = true;
		count++;
		sum += dis[j];

		//掃描當前頂點j的所有邊,進行鬆弛
		int k = first[j];//以j為起點的第一條邊的編號
		while (k != -1) {
			if (book[v[k]] == false && dis[v[k]] > w[k]) {
				dis[v[k]] = w[k];
				siftup(pos[v[k]],h,size,dis,pos);//對該點在堆中向上調整,因為該點對應的dis值變小了,所以只可能是向堆頂調整
			}
			k = next[k];
		}
	}

	cout << "sum=" << sum;

	
	system("pause");
}

    如果所有的邊權都不相等,那麼最小生成樹是唯一的。

    沒有使用堆優化的Prim演算法適用於稠密圖,使用了堆優化的Prim演算法則更適用於稀疏圖。