最小生成樹

最小生成樹及其性質

  最小生成樹是在一個給定的無向圖G(V,E)中求一棵樹T, 使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G中的邊,並且滿足整棵樹的邊權之和最小。
  最小生成樹有3 個性質需要掌握:
①最小生成樹是樹, 因此其邊數等於頂點數減1 , 且樹內一定不會有環。
②對給定的圖G(V,E), 其最小生成樹可以不唯一, 但其邊權之和一定是唯一的。
③由於最小生成樹是在無向圖上生成的, 因此其根結點可以是這棵樹上的任意一個結點。於是,如果題目中涉及最小生成樹本身的輸出, 爲了讓最小生成樹唯一, 一般都會直接給出根結點, 讀者只需以給出的結點作爲根結點來求解最小生成樹即可。

求解問題總結

給定一個無向圖,求其中包含了所有頂點的邊權最小之和的樹(和爲多少,什麼樣的樹)
 
 
 
求解最小生成樹一般有兩種算法, 即prim算法與kruskal算法。這兩個算法都是採用了
貪心法的思想, 只是貪心的策略不太一樣。

prim算法:類似Dijkstra(普里姆算法)

優缺點

優點:時間複雜度較低;缺點:思想較難理解
  prim算法與Dijkstra算法使用的思想幾乎完全相同, 只有在數組d[]的含義上有所區別。
其中,Dijkstra算法的數組d[]含義爲起點s到達頂點Vi的最短距離;
   而prim算法的數組d[]含義爲頂點Vi與集合S的最短距離;
  兩者的區別僅在於最短距離是頂點Vi針對“起點s”還是 “集合S”。另外,對最小生成樹問題而言, 如果僅是求最小邊權之和, 那麼在prim算法中就可以隨意指定一個頂點爲初始點, 例如在下面的代碼中將默認使用0號頂點爲初始點。
根據上面的描述, 可以得到下面的僞代碼(注意與prim算法基本思想進行聯繫):

//G爲圖, 一般設成全局變量;數組d爲頂點與集合S的最短距離
Prim(G, d[]) {
	初始化;
	for (循環n次) {
		u = 使d[u]最小的還未被訪問的頂點的標號;
		記u已被訪問;
		for (從u出發能到達的所有頂點V) {
			if (v未被訪問&&以u爲中介點使得v與集合S的最短距離d[v]更優) {
				將G[u][v]賦值給v與集合s的最短距離d[v];
			}
		}
	}
}

和Dijkstra算法的僞代碼進行比較後發現,Dijkstra算法和prim算法只有優化d[v]的部分不同,而其他語句都是相同的。這再次說明: Dijkstra算法和prim算法實際上是相同的思路,只不過是數組d[]的含義不同罷了。

應用案例

題1:數碼寶貝拯救者——討伐惡魔大陸

  這次亞歷山大的任務是討伐惡魔大陸。和精靈大陸一樣, 惡魔大陸也有六個城市, 但是城市的分佈與精靈大陸不同, 並且這裏城市之間的道路是雙向的。圖10-44a給出了惡魔大陸的六個城市(V0至V5) 和連接它們的無向邊, 邊上的數字表示距離(即邊權), 而城市結點的黑色表示還未被攻佔。
  由於惡魔大陸提前知道了亞歷山大要來攻打惡魔大陸, 因此惡魔們事先對所有道路進行了凍結, 希望以此消耗亞歷山大的體力去恢復這些道路。不過亞歷山大不會坐以待斃, 他在分析惡魔大陸的地圖之後打算從防守最薄弱的V0開始進攻,並且使用了“ 爆裂模式” 來對抗他們(圖10-44中)。在爆裂模式下, 亞歷山大可以隨時恢復任意一條已攻佔城市所連接的道路, 但是需要消耗那條道路的距離大小的體力(也就是說, 道路有多長, 就需要消耗多少體力去恢復)。並且在恢復某條道路之後, 亞歷山大會趁機攻佔這條道路所連接的未攻佔城市。
  爲了儘可能節省體力, 亞歷山大需要解決這樣的一個問題:如何選擇需要恢復的道路, 使得
亞歷山大可以消耗最少的體力, 並保證他可以攻佔所有城市。
在這裏插入圖片描述
  這其實就在求一棵最小生成樹
①首先,亞歷山大一定是每次從已攻佔城市出發去攻打未攻佔城市,這說明最後生成的結構一定連通。
②其次, 亞歷山大在把V0攻佔以後, 總是沿着一條新的道路去攻擊一個新的城市, 這說明最後生成的結構的邊數一定比頂點數少1。
  基於上面兩點, 最後生成的結構一定是一棵樹(是滿足了連通、邊數等於頂點數減1),而亞歷山大的要求就是使這棵樹的邊權之和最小。當然, 如果上面的解釋沒有看懂的話, 也不妨先記住:這裏求的就是一棵以V0爲根結點的最小生成樹,且接下來亞歷山大所做的每一步都是prim算法的步驟。
  在這裏,亞歷山大對地圖做出了三個修改
① 將地圖上的所有邊都抹去, 只有當攻佔一個城市後才把這個城市連接的邊顯現(這一點和Dijkstra算法中相同)。
②使用“ 爆裂模式” 的能蜇, 將已攻佔的城市置於一個巨型防護罩中。亞歷山大可以沿着這個防護罩連接的道路去進攻未攻佔的城市。
③在地圖中的城市Vi (0<=i<=5)上記錄城市Vi 與巨型防護罩之間的最短距離(即vi 與每個已攻佔城市之間距離的最小值)。由於在①中亞歷山大把所有邊都抹去了,因此在初始狀態下只在城市V0上標記0, 而其他城市都標記無窮大(記爲INF, 見圖10-44b)。爲了方便敘述,在下文中某幾處出現的最短距離都是指從城市vi 與當前巨型防護罩之間的最短距離。

輸入
  第一行輸入n:城市數(城市編號爲0~n-1);m:邊數;s:起點
  接下來m行輸入邊和邊權x,y,z:城市x,y,道路長度z

輸出
  消耗的最小體力爲多少

輸入情況:
6 10
0 1 4
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3

輸出情況:
15

AC代碼
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int maxv=1000;
map<int,int>adj[maxv];//頂點->邊權
int n,m,d[maxv];//n城市數,m道路數,城市號爲0~n-1
bool vis[maxv];

void init()
{
    for(int i=0;i<n;i++){
        vis[i]=false;
        d[i]=INF;
        adj[i].clear();
    }
}
int prim()
{
    int ans=0;//存放最小生成樹的邊權之和
    d[0]=0;
    for(int i=0;i<n;i++){
        int u=-1,MIN=INF;
        for(int j=0;j<n;j++){
            if(!vis[j]&&d[j]<MIN){
                u=j;
                MIN=d[j];
            }
        }
        if(u==-1)return -1;
        vis[u]=true;
        ans+=d[u];//把與集合S距離最短的這個邊加入最小生成樹
        for(map<int,int>::iterator it=adj[u].begin();it!=adj[u].end();it++){
                int v=it->first,dis=it->second;
                if(!vis[v]&&dis<d[v])
                    d[v]=dis;
        }
    }
    return ans;
}
int main(){
    int x,y,z;
    while(scanf("%d",&n)!=EOF){
        if(n==0)break;
        scanf("%d",&m);
        init();
        for(int i=0;i<m;i++){
            scanf("%d %d %d",&x,&y,&z);
            adj[x][y]=adj[y][x]=z;
        }
        printf("%d\n",prim());
    }
    return 0;
}

題2:問題 A: 還是暢通工程

http://codeup.cn/problem.php?cid=100000622&pid=0

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxv = 1000;
map<int, int>adj[maxv];//頂點->邊權
int n, m, d[maxv];//n城市數,m道路數,城市號爲0~n-1
bool vis[maxv];

void init()
{
	for (int i = 1; i<=n; i++){
		vis[i] = false;
		d[i] = INF;
		adj[i].clear();
	}
}
int prim()
{
	int ans = 0;//存放最小生成樹的邊權之和
	d[1] = 0;
	for (int i = 1; i<=n; i++){
		int u = -1, MIN = INF;
		for (int j = 1; j<=n; j++){
			if (!vis[j] && d[j]<MIN){
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return -1;
		vis[u] = true;
		ans += d[u];//把與集合S距離最短的這個邊加入最小生成樹
		for (map<int, int>::iterator it = adj[u].begin(); it != adj[u].end(); it++){
			int v = it->first, dis = it->second;
			if (!vis[v] && dis<d[v])
				d[v] = dis;
		}
	}
	return ans;
}
int main(){
	int x, y, z;
	while (scanf("%d", &n) != EOF){
		if (n == 0)break;
		m=n*(n-1)/2;
		init();
		for (int i = 0; i<m; i++){
			scanf("%d %d %d", &x, &y, &z);
			adj[x][y] = adj[y][x] = z;
		}
		printf("%d\n", prim());
	}
	return 0;
}

題3:問題 D: 繼續暢通工程

http://codeup.cn/problem.php?cid=100000622&pid=3

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxv = 1000;
struct node{
	int w;
	int ok;
};
map<int, node>adj[maxv];//頂點->邊權
int n, m, d[maxv];//n城市數,m道路數,城市號爲0~n-1
bool vis[maxv];

void init()
{
	for (int i = 1; i <= n; i++){
		vis[i] = false;
		d[i] = INF;
		adj[i].clear();
	}
}
int prim()
{
	int ans = 0;//存放最小生成樹的邊權之和
	d[1] = 0;
	for (int i = 1; i <= n; i++){
		int u = -1, MIN = INF;
		for (int j = 1; j <= n; j++){
			if (!vis[j] && d[j]<MIN){
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return -1;
		vis[u] = true;
		ans += d[u];//把與集合S距離最短的這個邊加入最小生成樹
		for (map<int, node>::iterator it = adj[u].begin(); it != adj[u].end(); it++){
			int v = it->first;
			node dis = it->second;
			if (!vis[v]){
				if (dis.ok == 1)
					d[v] = 0;
				else if (dis.w<d[v])
					d[v] = dis.w;
			}
		}
	}
	return ans;
}
int main(){
	int x, y, z, h;
	while (scanf("%d", &n) != EOF){
		if (n == 0)break;
		m = n*(n - 1) / 2;
		init();
		for (int i = 0; i<m; i++){
			scanf("%d %d %d %d", &x, &y, &z, &h);
			adj[x][y].w = adj[y][x].w = z;
			adj[x][y].ok = adj[y][x].ok = h;
		}
		printf("%d\n", prim());
	}
	return 0;
}

題4:Freckles

題目:https://ac.nowcoder.com/acm/problem/115648
  In an episode of the Dick Van Dyke show, little Richie connects the freckles on his Dad’s back to form a picture of the Liberty Bell. Alas, one of the freckles turns out to be a scar, so his Ripley’s engagement falls through.
  Consider Dick’s back to be a plane with freckles at various (x, y) locations. Your job is to tell Richie how to connect the dots so as to minimize the amount of ink used. Richie connects the dots by drawing straight lines between pairs, possibly lifting the pen between lines. When Richie is done there must be a sequence of connected lines from any freckle to any other freckle.
Input
  The input begins with a single positive integer on a line by itself indicating the number of the cases following, each of them as described below. This line is followed by a blank line, and there is also a blank line between two consecutive inputs.
  The first line contains 0 < n ≤ 100, the number of freckles on Dick’s back. For each freckle, a line follows; each following line contains two real numbers indicating the (x, y) coordinates of the freckle.
Output
  For each test case, the output must follow the description below. The outputs of two consecutive cases will be separated by a blank line.
  Your program prints a single real number to two decimal places: the minimum total length of ink lines that can connect all the freckles.
Sample Input
1
3
1.0 1.0
2.0 2.0
2.0 4.0
Sample Output
3.41

思路

題目的意思就是有n個頂點(雀斑),要求用最少的墨水將所有雀斑都連在一起
理解爲:把所有雀斑都連接在一起成爲無向圖,邊權就是邊的長度,求最小生成樹
步驟:

  1. 輸入所有點的x,y座標
  2. 遍歷所有兩個點的組合所構成的線(n個頂點共能組成n*(n-1)/2個邊)
 for(int i=0; i<n; i++) {
        for(int j=i+1; j<n; j++)
            adj[i][j]=adj[j][i]=compute(i,j);
    }
  1. 計算邊長(知道兩個點的座標計算邊長)
double compute(int a,int b) { //溝谷,知兩直角邊求斜邊
    return sqrt(pow((x[a]-x[b]),2)+pow((y[a]-y[b]),2));
}
  1. prime算法
  2. **注意:**輸出格式是要每組答案之間留一空行,但最後沒有空行
      For each test case, the output must follow the description below. The outputs of two consecutive cases will be separated by a blank line.
AC代碼
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<map>
using namespace std;
const double INF = 1e17;
const double eps=1e-9;
const int maxv = 1000;
double x[maxv],y[maxv];//(0~n-1)雀斑的x,y座標
map<int,double>adj[maxv];//點(id1,id2)->長度
int n;
double d[maxv];
bool vis[maxv];

void init() {
    for(int i=0; i<n; i++) {
        vis[i]=false;
        d[i]=INF;
    }
}
double compute(int a,int b) { //溝谷,知兩直角邊求斜邊
    return sqrt(pow((x[a]-x[b]),2)+pow((y[a]-y[b]),2));
}
void scan() {
    init();
    for (int i = 0; i<n; i++)
        scanf("%lf %lf",&x[i],&y[i]);
    for(int i=0; i<n; i++) {
        for(int j=i+1; j<n; j++)
            adj[i][j]=adj[j][i]=compute(i,j);
    }
}
double prime() {
    double ans=0;
    d[0]=0;
    for(int i=0; i<n; i++) {
        int u=-1;
        double MIN=INF;
        for(int j=0; j<n; j++) {
            if(!vis[j]&&(d[j]-MIN)<eps) {
                MIN=d[j];
                u=j;
            }
        }
        if(u==-1)
            return -1;
        vis[u]=true;
        ans+=d[u];
        for(map<int,double>::iterator it=adj[u].begin(); it!=adj[u].end(); it++) {
            int v=it->first;
            double dis=it->second;
            if(!vis[v]&&(dis-d[v])<eps)
                d[v]=dis;
        }
    }
    return ans;
}
int main() {
    double x, y;
    int sum;
    scanf("%d",&sum);
    while (sum--) {
        scanf("%d", &n);
        scan();
        printf("%.2lf\n",prime());
        if(sum)
            printf("\n");
    }
    return 0;
}

題5:問題 E: Jungle Roads

http://codeup.cn/problem.php?cid=100000622&pid=4

#include<bits/stdc++.h>
using namespace std;
const int INF = 1e9;
const int maxv = 50;
map<int, int>adj[maxv];
int n, d[maxv];
bool vis[maxv];

void init() {
	for (int i = 1; i <= n; i++){
		d[i] = INF;
		vis[i] = false;
		adj[i].clear();
	}
}
void scan() {
	init();
	int x, y, z, m;
	char t;
	for (int i = 1; i<n; i++){
		getchar();
		scanf("%c %d", &t, &m);
		x = (int)(t - 'A' + 1);
		while (m--){
			scanf(" %c %d", &t, &z);
			y = (int)(t - 'A' + 1);
			adj[x][y] = adj[y][x] = z;
		}
	}
}
int prime() {
	int ans = 0;
	d[1] = 0;
	for (int i = 1; i <= n; i++) {
		int u = -1, MIN = INF;
		for (int j = 1; j <= n; j++) {
			if (!vis[j] && d[j]<MIN) {
				MIN = d[j];
				u = j;
			}
		}
		if (u == -1)
			return -1;
		vis[u] = true;
		ans += d[u];
		for (map<int, int>::iterator it = adj[u].begin(); it != adj[u].end(); it++) {
			int v = it->first, dis = it->second;
			if (!vis[v] && dis<d[v])
				d[v] = dis;
		}
	}
	return ans;
}
int main() {
	while (scanf("%d", &n) != EOF) {
		if (n == 0)break;
		scan();
		printf("%d\n", prime());
	}
	return 0;
}

kruskal 算法:邊貪心策略(克魯斯卡爾算法")

優缺點

優點:思想簡潔;缺點:時間複雜度較高
  其思想極其簡潔,理解難度比prim 算法要低很多。
  kruskal 算法的基本思想爲:在初始狀態時隱去圖中的所有邊,這樣圖中每個頂點都自成一個連通塊。之後執行下面的步驟:

  1. 對所有邊按邊權從小到大進行排序。
  2. 按邊權從小到大測試所有邊,如果當前測試邊所連接的兩個頂點不在同一個連通塊中, 則把這條測試邊加入當前最小生成樹中;否則, 將邊捨棄。
  3. 執行步驟(2),直到最小生成樹中的邊數等於總頂點數減l或是測試完所有邊時結束。而當結束時如果最小生成樹的邊數小於總頂點數減1,說明該圖不連通。

僞代碼

int kruskal() {
	令最小生成樹的邊權之和爲ans、最小生成樹的當前邊數Num Edge;
	將所有邊按邊權從小到大排序;
		for (從小到大枚舉所有邊) {
		if (當前測試邊的兩個端點在不同的連通塊中) {
			將該測試邊加入最小生成樹中;
			ans += 測試邊的邊權;
			最小生成樹的當前邊數Num Edge 加l;
			當邊數Num Edge 等於頂點數減1 時結束循環;
		}
		}
	return ans;
}

在這個僞代碼裏有兩個細節似乎不太直觀, 即

  1. 如何判斷測試邊的兩個端點是否在不同的連通塊中。(查詢)
  2. 如何將測試邊加入最小生成樹中。(合併)

  事實上,對這兩個問題,可以換一個角度來想。如果把每個連通塊當作一個集合,那麼就可以把問題轉換爲判斷兩個端點是否在同一個集合中, 而這個問題在前面討論過一—對,就是並查集。並查集可以通過查詢兩個結點所在集合的根結點是否相同來判斷它們是否在同一個集合,而合併功能恰好可以把上面提到的第二個細節解決,即只要把測試邊的兩個端點所在集合合併,就能達到將邊加入最小生成樹的效果。
  於是可以根據上面的解釋,把kruskal 算法的代碼寫出來(建議結合僞代碼學習)。另外,假設題目中頂點編號的範圍是[1,n], 因此在並查集初始化時範圍不能弄錯。如果下標從0開始, 則整個代碼中也只需要修改並查集初始化的部分即可。

應用案例

題一:討伐惡魔大陸

題目同討伐惡魔大陸
輸入
  第一行輸入n:城市數(城市編號爲0~n-1);m:邊數;s:起點
  接下來m行輸入邊和邊權x,y,z:城市x,y,道路長度z

輸出
  消耗的最小體力爲多少

輸入情況:
6 10
0 1 4
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3

輸出情況:
15

#include<bits/stdc++.h>
using namespace std;
const int maxv=1010;
const int INF=1e9;
struct edge { //邊
    int u,v;//兩個頂點
    int cost;//長度
} E[maxv];
bool cmp(edge a,edge b) {
    return a.cost<b.cost;
}
//並查集部分
int father[maxv],n,m;//並查集數組,頂點n個,邊m個
void initfather() {
    for(int i=0; i<n; i++)
        father[i]=i;
}
int findFather(int x) {
    int a=x;
    while(x!=father[x]) { //找到其所在集合的根節點爲x
        x=father[x];
    }
    while(a!=father[a]) { //路徑壓縮
        int z=a;
        a=father[a];
        father[z]=x;
    }
    return x;
}
bool Union(int a,int b) { //返回是否爲同一集合,若不爲則合併了
    int faA=findFather(a);
    int faB=findFather(b);
    if(faA!=faB) {
        father[faA]=faB;
        return true;
    }
    return false;
}
int kruskal()
{
    //kruskal 函數返回最小生成樹的邊權之和, 參數n 爲頂點個數, m 爲圖的邊數
    int ans=0,Num_Edge=0;
    initfather();//並查集初始化
    sort(E,E+m,cmp);//排序
    for(int i=0;i<m;i++){
        if(Union(E[i].u,E[i].v)){
            ans+=E[i].cost;
            Num_Edge++;
            //邊數等於頂點數減1 時結束算法(那時已包含了所有頂點在樹上)
            if(Num_Edge==(n-1))
                break;
        }
    }
    if(Num_Edge!=(n-1))//不足n-1就不能連接n個頂點,說明這個圖不是連通圖
        return -1;
    else
        return ans;
}
int main() {
    while(scanf("%d",&n)!=EOF){
        if(n==0)break;
        scanf("%d",&m);
        for(int i=0;i<m;i++)
            scanf("%d %d %d",&E[i].u,&E[i].v,&E[i].cost);
        printf("%d\n",kruskal());
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章