上周末的蓝桥杯省模拟赛时最后一题是一道最小生成树的题目,因为恰好在慕课上刚看到这个地方,所以现学了 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; | |
} |