banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

圖論——解決最小生成樹問題(Kruskal算法&Prim算法)

上週末的藍橋杯省模擬賽時最後一題是一道最小生成樹的題目,因為恰好在慕課上剛看到這個地方,所以現學了 Prim 算法,解決了這個題目 (大概),賽後就打算多琢磨琢磨這一類題目。題目鏈接
最小生成樹問題,是指給定無向圖 G=(V,E),連接 G 中所有點,且邊集是 E 的子集的樹稱為 G 的生成樹 (Spanning Tree),而權值最小的生成樹稱為最小生成樹 (Minimal Spanning Tree,MST)。

一、Kruskal 算法#

Kruskal 算法易於編寫,且效率很高
紫書 p356 描述如下:

Kruskal 算法的第一步是給所有邊按照從小到大的順序排列,這一步可以直接使用庫函數 qsort 或者 sort。接下來從小到大依次考察每條邊 (u, v)。

  • 情況 1:u 和 v 在同一個連通分量中,那麼加入 (u, v) 後會形成環,因此不能選擇。
  • 情況 2:如果 u 和 v 在不同的連通分量,那麼加入 (u, v) 一定是最優的。 為什麼呢?下面用反證法 —— 如果不加這條邊能得到一個最優解 T, 則 T+(u, v) 一定有且只有一個環,而且環中至少有一條邊 (u', v') 的權值大於或等於 (u, v) 的權值。刪除該邊後,得到的新樹 T' = T + (u, v) - (u', v') 不會比 T 更差。因此,加入 (u, v) 不會比不加入差。

下面是伪代码:

把所有邊(按權值)排序,記第i小的邊為e[i](1 <= i < m)
初始化MST為空
初始化連通分量,讓每個點自成一個獨立的連通分量
for(int i = 0; i < m; i++) {
	if(e[i].u 和 e[i].v 不在同一個連通分量) {
		把邊e[i]加入MST
		合併e[i].u和e[i].v所在的連通分量
	}
}

在上面的伪代码中,最關鍵的地方在於 “連通分量的查詢與合併” :需要知道任意兩個點是否在同一連通分量中,還需要合併這兩個連通分量。
有一種簡潔高效的算法可以用來處理這個問題 ——並查集
此處指路大佬解釋:超有愛的並查集~
將每個連通分量看作一個集合,該集合包含了連通分量中的所有點。這些點兩兩相通,而具體的連通方式無關緊要。在圖中,每個點都恰好屬於一個連通分量,對應到集合表示中,即為每個元素恰好屬於一個集合。換句話說,圖的所有連通分量可以用若干個不相交集合來表示。
奉上自己的並查集板子

#include <iostream>
using namespace std;
#define div 1000000007
typedef long long ll;
const int maxn = 1001;
int fa[maxn];//
inline void init(int n) {
    for (int i = 1; i <= n; i++)
        fa[i] = i;
}
int find(int x) {//查詢+路徑壓縮 把沿途的每個節點的父節點都設為根節點
    return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
inline void merge(int i, int j) {
    fa[find(i)] = find(j);//前者的父節點設為後者
}

有了並查集之後,Kruskal 算法的完整代碼便不難給出。假設第 i 條邊的兩個端點序號和權值分別保存在 u [i],v [i] 和 w [i] 中,而排序後第 i 小的邊的序號保存在 r [i] 中 (間接排序,即排序的關鍵字是對象的 “代號”,而不是對象本身),p 為並查集

完整代碼:#

int cmp(const int i, const int j) {	return w[i] < w[j]; }	//間接排序函數
int find(int x) { return p[x] == x ? x : p[x] = find(p[x])}	//並查集的find
int Kruskal() {
	int ans = 0;
	for(int i = 0; i < n; i++) p[i] = i;	//初始化並查集
	for(int i = 0; i < m; i++) r[i] = i;
	sort(r,r+m,cmp);
	for(int i = 0; i < m; i++) {
		int e = r[i];
		int x = find(u[e]);//找出一個端點所在集合編號
		int y = find(v[e]);//找出另一個端點所在集合編號
		if(x != y) {		//若在不同集合,合併
			ans += w[e];
			p[x] = y;
		}
	}
	return ans;
}

練習題:#

題號 | 題目名稱 (英文)| 備註 |
|--|--|--|--|
hdu1863| 畅通工程 | 最小生成樹 板子題
hdu1879| 继续畅通工程 | 最小生成樹 板子題
hdu1875| 畅通工程再续 | 最小生成樹 板子題
洛谷 P3366| 【模板】最小生成樹 | 最小生成樹 板子題

1. hdu1863 畅通工程#

Problem Description
省政府 “畅通工程” 的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。经过调查评估,得到的统计表中列出了有可能建设公路的若干条道路的成本。现请你编写程序,计算出全省畅通需要的最低成本。
Input
测试输入包含若干测试用例。每个测试用例的第 1 行给出评估的道路条数 N、村庄数目 M (< 100);随后的 N
行对应村庄间道路的成本,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间道路的成本(也是正整数)。为简单起见,村庄从 1 到 M 编号。当 N 为 0 时,全部输入结束,相应的结果不要输出。
Output
对每个测试用例,在 1 行里输出全省畅通需要的最低成本。若统计数据不足以保证畅通,则输出 “?”。

#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 105;
int N,M,cnt;
int u[maxn],v[maxn],w[maxn];
int r[maxn],fa[maxn];
bool cmp(const int r1, const int r2) {
    return w[r1] < w[r2];
}
int find(int x) {
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int Kruskal() {
    int ans = 0;
    for(int i = 0; i < M; i++) fa[i] = i;//初始化并查集,让每个点自成一个连通分量
    for(int i = 0; i < N; i++) r[i] = i;//存储边序号
    sort(r, r+N, cmp);//将边从小到大按权值排序
    for(int i = 0; i < N; i++) {
        int e = r[i];
        int x = find(u[e]);
        int y = find(v[e]);
        if(x != y) {
            ans += w[e];
            cnt++;
            fa[x] = y;
        }
    }
    return ans;
}
int main() {
    while(cin >> N >> M) {
        cnt = 0;
        if(N == 0) break;
        for(int i = 0; i < N; i++) {
            cin >> u[i] >> v[i] >> w[i];
        }
        int ans = Kruskal();
        if(cnt != M-1) cout << "?" << endl;
        else cout << ans << endl;
    }
    return 0;
}

2.hdu1879 继续畅通工程#

這題相比上題來說,就將已經建好的道路花費置為 0 即可,同時注意本題輸入輸出好像都不能用 cincout, 會超時。

Problem Description
省政府 “畅通工程” 的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。现得到城镇道路统计表,表中列出了任意两城镇间修建道路的费用,以及该道路是否已经修通的状态。现请你编写程序,计算出全省畅通需要的最低成本。
Input
测试输入包含若干测试用例。每个测试用例的第 1 行给出村庄数目 N (1< N < 100);随后的 N (N-1)/2 行对应村庄间道路的成本及修建状态,每行给 4 个正整数,分别是两个村庄的编号(从 1 编号到 N),此两村庄间道路的成本,以及修建状态:1 表示已建,0 表示未建。
当 N 为 0 时输入结束。
Output
每个测试用例的输出占一行,输出全省畅通需要的最低成本。

#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 5008;
int fa[101];
int find(int x) {
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}
struct Edge {
public:
    int u, v, w;
    bool operator<(const Edge &a)const{ 
        return w < a.w;
    }
}E[maxn];

int main() {
    int N;
    while(scanf("%d", &N) != EOF) {
        int ans = 0;
        if(N == 0) break;
        int M = N*(N-1)/2;
        for(int i = 1; i <= M; ++i) {
            int flag;
            scanf("%d%d%d%d", &E[i].u, &E[i].v, &E[i].w, &flag);
            if(flag) E[i].w = 0;
        }
        for(int i = 1; i <= N; ++i) fa[i] = i;//初始化并查集,让每个点自成一个连通分量
        sort(E+1,E+1+M);//将边从小到大按权值排序
        for(int i = 1; i <= M; ++i) {
            int x = find(E[i].u);
            int y = find(E[i].v);
            if(x != y) {
                fa[x] = y;
                ans += E[i].w;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

3.hdu1875 畅通工程再续#

先輸入所有頂點,再判斷每兩個頂點是否可以構成合適的邊

Problem Description
相信大家都聽說一個 “百島湖” 的地方吧,百島湖的居民生活在不同的小島中,當他們想去其他的小島時都要通過划小船來實現。現在政府決定大力發展百島湖,發展首先要解決的問題當然是交通問題,政府決定實現百島湖的全暢通!經過考察小組 RPRush 對百島湖的情況充分了解後,決定在符合條件的小島間建上橋,所謂符合條件,就是 2 個小島之間的距離不能小於 10 米,也不能大於 1000 米。當然,為了節省資金,只要求實現任意 2 個小島之間有路通即可。其中橋的價格為 100 元 / 米。
Input
輸入包括多組數據。輸入首先包括一個整數 T (T <= 200),代表有 T 組數據。
每組數據首先是一個整數 C (C <= 100),代表小島的個數,接下來是 C 組坐標,代表每個小島的坐標,這些坐標都是 0 <= x, y <= 1000 的整數。
Output
每組輸入數據輸出一行,代表建橋的最小花費,結果保留一位小數。如果無法實現工程以達到全部暢通,輸出”oh!”.

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn = 5008;
typedef long long ll;
struct Point {
    int x,y;
} V[101];
struct Edge {
public:
    int u, v;
    double w;
    bool operator<(const Edge &a)const{ 
        return w < a.w;
    }
}E[maxn];
int fa[105];
int find(int x) {
    return fa[x] == -1 ? x : fa[x] = find(fa[x]);
}
int main() {
    int T;
    scanf("%d", &T);
    while(T--) {
        int N;
        scanf("%d", &N);
        for(int i = 0; i < N; ++i) {
            scanf("%d%d", &V[i].x, &V[i].y);
        }
        int k = 0;
        for(int i = 0; i < N-1; ++i) {
            for(int j = i+1; j < N; ++j) {
                double d = sqrt( (V[i].x-V[j].x)*(V[i].x-V[j].x) + 
                (V[i].y-V[j].y)*(V[i].y-V[j].y) );
                if (d <= 1000 && d >= 10) {
                    E[k].u = i;
                    E[k].v = j;
                    E[k].w = d*100;
                    k++;
                }
            }
        }
        for(int i = 0; i < N; ++i) fa[i] = -1;//初始化并查集,让每个点自成一个连通分量
        sort(E,E+k);//将边从小到大按权值排序
        double ans = 0;
        int cnt = 0;
        for(int i = 0; i < k; ++i) {
            int x = find(E[i].u);
            int y = find(E[i].v);
            if(x != y) {
                fa[x] = y;
                ans += E[i].w;
                cnt++;
            }
        }
        if (cnt == N-1) printf("%.1f\n",ans);
        else printf("oh!\n");
    }
    return 0;
}

4.洛谷 P3366【模板】最小生成树 #

板子題

#include <iostream>
#include <algorithm>
using namespace std;
const int maxm = 200000;
int fa[5005];//最大頂點數5000
int N,M,cnt;//頂點數 邊數
struct edge {
    int u,v,w;
    bool operator<(const edge& a) const {
        return w < a.w;
    }
} E[maxm];//最大邊數maxn
int find(int x) {
    return fa[x] == -1 ? x : fa[x] = find(fa[x]);
}
int Kruskal() {
    for(int i = 0; i < N; ++i) fa[i] = -1;
    int ans = 0;
    sort(E,E+M);
    for(int i = 0; i < M; ++i) {
        int x = find(E[i].u);
        int y = find(E[i].v);
        if(x != y) {
            fa[x] = y;
            ans += E[i].w;
            cnt++;
        }
    }
    if(cnt == N-1) return ans;
    else return -1;
}
int main() {
    scanf("%d%d", &N, &M);
    for(int i = 0; i < M; ++i) {
        scanf("%d%d%d", &E[i].u, &E[i].v,&E[i].w);
    }
    int ans = Kruskal();
    if(ans == -1) printf("orz\n");
    else printf("%d\n",ans);
    return 0;
}

二、Prim 算法#

來自陳越姥姥的數據結構
Prim 算法基本思路 —— 從一個根節點開始,讓一棵小樹長大。與 Dijkstra 算法相近。
Dijkstra 算法伪代码:

void Dijkstra(Vertex s) {
	while(1) {
		V = 未收錄頂點中dist最小者
		if (這樣的V不存在)
			break;
		collected[V] = true;//將V收錄
		for(V的每個鄰接點W) {
			if(collected[W] == false && dist[V] + E(V,W) < dist[W]) {
				dist[W] = dist[V]+E<V,W>;
				path[W] = V;
			} 
		}
}

Prim 算法伪代码:

void Prim(Vertex s) {
	while(1) {
		V = 未收錄頂點中dist最小者
		if (這樣的V不存在)
			break;
		將V收錄進MST
		for(V的每個鄰接點W) {
			if(W未被收錄 && E(V,W) < dist[W]) {
				dist[W] = E<V,W>;
				parent[W] = V;
			} 
		}
}

Prim 算法完整代碼#

double edge[maxn][maxn];//存邊的權值~
int vis[maxn];//該點是否已訪問過
double dist[maxn];
double ans;
double Prim() {
    for(int i = 2; i <= n; i++) {
        dist[i] = edge[1][i];//初始化dist為根結點1到所有點的距離
    }
    vis[1] = 1;////收錄初始點1 vis[i] == 0表示還未被收錄
    for(int i = 2; i <= n; i++) {
        int min = INF;
        int v = -1;
        for(int j = 2; j <= n; j++) {//找v————未收錄頂點中dist最小者
            if(!vis[j] && dist[j] < min) {
                min = dist[j];
                v = j;
            }
        }
        if(v != -1) {//找到了v~收入MST
            vis[v] = 1;
            ans += dist[v];
            for(int j = 2;j <= n; j++) {//更新距離dist
                if(!vis[j] && edge[v][j] < dist[j]) {//當這點未被訪問且到任意一點的距離比現在到樹的距離小就更新
                    dist[j] = edge[v][j];
                }
            }
        }
    }
    return ans;
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。