1. 程式人生 > >鏢局運鏢---無向圖的最小生成樹

鏢局運鏢---無向圖的最小生成樹

  假設有n個城市和m條道路,對應無向圖中的點和邊。每條路的過路費對應邊的權值。鏢局現在需要選擇一些道路進行疏通,以便邊距可以達到任意一個城鎮,要求花費的銀子越少越好。換句話說,鏢局的要求就是用最少的邊讓圖連通(任意兩點之間可以互相到達),將多餘的邊去掉。

  很顯然,要想讓有n個頂點的圖連通,那麼至少需要n-1條邊。如果一個連通無向圖不包含迴路,那麼就是一棵樹,其實這裡就是求一個圖的最小生成樹。這裡我們僅討論無向圖的最小生成樹。

  既然要求讓邊的總權值最小,自然可以想到首先選擇最短的邊,然後選擇次短的邊……直到選擇了n-1條邊為止。這就需要先對所有的邊按照權值大小進行從小到大的排序,然後從最小的開始選,依次選擇每一條邊,直到選擇了n-1條邊讓整個圖連通為止。中間新增每一條邊時還需要判斷這條邊對應的兩個頂點是否已經連通,如果已經連通則捨棄這條邊,進行下一條邊的判斷。

  判斷兩個頂點是否已經連通,可以使用深度優先搜尋或者廣度優先搜尋,但這樣效率很低。更好的選擇是使用並查集,將所有頂點放入一個並查集中,判斷兩個頂點是否連通,僅需要判斷兩個頂點是否在同一個集合(即是否有共同的祖先)即可,這樣時間複雜度僅為O(logN)。

  這個演算法名為Kruskal,總結如下:首先按照邊的權值進行從小到大的排序,每次從剩餘的邊中選擇權值較小且邊的兩個頂點不在同一個集合內的邊(就是不會產生迴路的邊),加入到生成樹中,直到加入了n-1條邊為止,程式碼如下:

struct edge {
	int u;
	int v;
	int w;
}; //為了方便排序,用一個結構體來儲存邊的關係

vector<struct edge> e;//用於儲存各邊資訊
vector<int> f;//並查集需要用到

//快速排序 形參left是需要快排的陣列的最左邊元素的序號,right是需要快排的陣列最右邊元素的序號
void quicksort(int left, int right) {
	int i = left, j = right;
	struct edge temp;
	if (left > right)
		return;

	while (i != j) {
		//順序很重要,因為我們設的基點是最左邊的那個元素,所以從右邊開始找
		while (e[j].w >= e[left].w && i < j) {
			j--;
		}
		while (e[i].w <= e[left].w && i < j) {
			i++;
		}
		if (i != j) {
			temp = e[i];
			e[i] = e[j];
			e[j] = temp;
		}
	}

	//將基準數歸位(此時i已經等於j)
	temp = e[left];
	e[left] = e[i];
	e[i] = temp;

	//繼續處理左邊的,這是一個遞迴的過程
	quicksort(left, i - 1);
	//繼續處理右邊的,這是一個遞迴的過程
	quicksort(i + 1, right);
	return;
}

//並查集中尋找祖先的函式
int getf(int v) {
	if (f[v] == v)
		return v;
	else {
		//這裡是路徑壓縮,每次在函式返回的時候,順帶把路上遇到的人的祖先改為最後找到的祖先編號
		f[v] = getf(f[v]);
		return f[v];
	}
}

//並查集合並兩個子集的函式
bool merge(int v, int u) {
	int t1, t2;//t1,t2分別為v和u的大boss,每次雙方的會談都必須是各自的最高領導人才行
	t1 = getf(v);
	t2 = getf(u);
	if (t1 != t2) {
		//如果v和u兩個節點的祖先不同
		//採用"靠左原則“,左邊變成右邊的祖先。即把右邊的集合,作為左邊集合的子集合。
		f[t2] = t1;
		return true;
	}
	return false;//v和u的祖先相同,即已經連通了,返回false
}


int main()
{
	int n, m; //n表示頂點個數,m表示邊的條數
	int count=0, sum=0;//count 儲存當前已選了多少條邊; sum儲存最終選用的所有邊的總權重
	cin >> n >> m;
	e.resize(m);
	f.resize(n, 0);

	//讀入邊
	for (int i = 0; i < m; i++) {
		//u:起點 v:終點 w:權值
		cin >> e[i].u >> e[i].v >> e[i].w;
	}

	//按照邊的權值大小對邊進行從小到大的快速排序
	quicksort(0, m - 1);

	//並查集初始化
	for (int i = 0; i < n; i++) {
		f[i] = i;
	}

	//Kruskal演算法核心部分
	for (int i = 0; i < m; i++) {
		//判斷一條邊的兩個頂點是否已經連通,即判斷是否已在同一個集合中
		if (merge(e[i].u, e[i].v)) {
			//目前尚未連通,選用這條邊
			count++;
			sum += e[i].w;
		}

		if (count == n - 1) //對n個頂點的圖只需要n-1條邊就可以全連通了
			break;
	}
	
	cout << "sum=" << sum;
	
	system("pause");
}

結果:

現在來討論Kruskal演算法的時間複雜度。對邊進行快速排序是O(MlogM),再m條邊中找出n-1條邊是O(MlogN),所以總的時間複雜度為O(MlogM+MlogN)。通常M比N要大很多,因此最終時間複雜度為O(MlogM)。