在学习最小生成树算法之前,要先了解相关知识点!
-
生成树: 一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
-
最小生成树: 在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
一、普里姆算法(Prim)
算法思路:
-
首先就是从图中的一个起点a开始,把a加入U集合。
-
然后,寻找从与a有关联的边中,权重最小的那条边并且该边的终点b在顶点集合(V-U)中,把 b 也加入到集合U中,并且输出边(a,b)的信息,这样集合U就有 {a,b}。
-
接着,寻找与 a 关联和 b 关联的边中,权重最小的那条边并且该边的终点在集合(V-U)中,把 c 加入到集合U中,并且输出对应的那条边的信息,这样集合U就有 {a,b,c} 这三个元素了。
-
依次类推,直到所有顶点都加入到了集合U。
下面我们用 Prim 算法对下面这幅图求其最小生成树:
- 假设从顶点 v1 开始,可以发现(v1,v3)边的权重最小,所以第一个输出的边就是:v1—v3=1:
- 然后,从 v1 和 v3 作为起点的边中寻找权重最小的边,首先边(v1,v3)已经访问过了,所以从其他边中寻找,发现 (v3,v6) 这条边最小,所以输出边就是:v3—-v6=4
- 然后,从 v1、v3、v6 这三个点相关联的边中寻找一条权重最小的边,可以发现边 (v6,v4) 权重最小,所以输出边就是:v6—-v4=2.
- 然后,从 v1、v3、v6、v4 这四个顶点相关联的边中寻找权重最小的边,发现边(v3,v2)的权重最小,所以输出边:v3—–v2=5
- 然后,从 v1、v3、v6、v4,v2 这五个顶点相关联的边中寻找权重最小的边,发现边(v2,v5)的权重最小,所以输出边:v2—–v5=3
- 最后,我们发现六个点都已经加入到集合U了,我们的最小生成树建立完成。
二、克鲁斯卡算法(Kruskal)
算法思路:
-
将图中的所有边都去掉。
-
将边按权值从小到大的顺序添加到图中,保证添加的过程中不会形成环
-
重复上一步直到连接所有顶点,此时就生成了最小生成树。这是一种贪心策略。
下面我们用 Kruskal 算法对下面这幅图求其最小生成树:
-
首先从这些边中找出权重最小的那条边,可以发现边(v1,v3)这条边的权重是最小的,所以我们输出边:v1—-v3=1
-
然后,我们需要在剩余的边中,再次寻找一条权重最小的边,可以发现边(v4,v6)这条边的权重最小,所以输出边:v4—v6=2
- 然后,我们再次从剩余边中寻找权重最小的边,发现边(v2,v5)的权重最小,所以可以输出边:v2—-v5=3
- 然后,我们使用同样的方式找出了权重最小的边:(v3,v6),所以我们输出边:v3—-v6=4
- 我们还需要找出最后一条边就可以构造出一颗最小生成树,但是这个时候我们有三个选择:(v1,V4),(v2,v3),(v3,v4),这三条边的权重都是5,首先我们如果选(v1,v4)的话,得到的图如下:
- 我们发现,这肯定是不符合我们算法要求的,因为它出现了一个环,所以我们再使用第二个(v2,v3)试试,得到图形如下:
- 我们发现,这个图中没有环出现,而且把所有的顶点都加入到了这颗树上了,所以(v2,v3)就是我们所需要的边,所以最后一个输出的边就是:v2—-v3=5
三、例题
原题链接:还是畅通工程
思路: 最小生成树模板题。
Prim 做法:
// 很神奇,用Prime算法Java就可以过
import java.util.Scanner;
public class Main {
static int N = 105, INF = 0x3f3f3f3f;
static int n, m;
static int[] dis = new int[N]; // dis[i]表示i点到生成树集的最短距离
static boolean[] vis = new boolean[N];
static int[][] e = new int[N][N];
static int Prime() {
for (int i = 1; i <= n; i++) {
dis[i] = e[1][i]; // 初始为1到其他顶点距离
}
dis[1] = 0;
vis[1] = true; // 将用过的点标记,避免重复访问
int ans = 0;
for (int i = 1; i < n; i++) { // 进行n-1次操作
int minn = INF;
int pos = 1; // 记录最小权值的顶点
for (int j = 1; j <= n; j++) { // 在未加入点中,找到一个最小的权值
if (!vis[j] && minn > dis[j]) {
minn = dis[j];
pos = j;
}
}
if (minn == INF) // 不是连通图
break;
vis[pos] = true; // 将加入点进行标记
ans += minn; // 加边权
for (int j = 1; j <= n; j++) { // 在未加入点中找到与此点相连的权值最小的边
if (!vis[j] && dis[j] > e[pos][j])
dis[j] = e[pos][j];
}
}
return ans;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
for (int i = 1; i <= 101; i++)
vis[i] = false;
n = sc.nextInt();
if (n == 0)
break;
m = n * (n - 1) / 2;
for (int i=1;i<=n;i++) {
for (int j=1;j<=n;j++) {
e[i][j] = INF;
}
}
for (int i = 1; i <= m; i++) {
int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
e[u][v] = e[v][u] = w;
}
System.out.println(Prime());
}
}
}
Kruskal 做法:
//c++做法,能AC
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 105;
int n,m,f[N];// f[i]表示第i点的祖先
struct node{
int u,v,w;
}e[N*N]; //定义边
bool cmp(node a,node b){
return a.w < b.w;
}
int find(int u){
return f[u]==u?u:find(f[u]);
}
int kruskal(){
for(int i=1;i<=n;i++)
f[i] = i; //初始化,开始时每个村庄都是单独的集
sort(e+1,e+m+1,cmp);
int ans = 0;
for(int i=1;i<=m;i++){
int fu = find(e[i].u); //边的前端点所在的集合
int fv = find(e[i].v); //边的后端点所在的集合
if(fu != fv){ //不在同一个集合,代表加入此边不会形成一个环
f[fu] = fv; //合并
ans+=e[i].w;
}
}
return ans;
}
int main() {
while(cin>>n && n) {
m = n*(n-1)/2;
for(int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v>>e[i].w;
}
cout<<kruskal()<<endl;
}
return 0;
}
//Java做法,会TLE,估计这道题专门卡点了吧
import java.util.Arrays;
import java.util.Scanner;
class node implements Comparable<node> {
int u, v, w;
node() {
};
public node(int u, int v, int w) {
this.u = u;
this.v = v;
this.w = w;
}
public int compareTo(node a) {
return this.w - a.w;
}
}
public class Main {
static int n, m;
static node[] a = new node[100100];
static int[] f = new int[100100];
static int find(int u) {
return f[u] == u ? u : find(f[u]);
}
static int kruskal() {
for (int i = 1; i <= m; i++) {
f[i] = i;
}
Arrays.sort(a, 0, m);
int ans = 0;
for (int i = 1; i <= m; i++) {
int fu = find(a[i].u);
int fv = find(a[i].v);
if (fu != fv) {
f[fu] = fv;
ans += a[i].w;
}
}
return ans;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
n = sc.nextInt();
if (n == 0)
break;
m = n * (n - 1) / 2;
for (int i = 0; i <= 100010; i++)
a[i] = new node();
for (int i = 1; i <= m; i++) {
a[i] = new node(sc.nextInt(), sc.nextInt(), sc.nextInt());
}
System.out.println(kruskal());
}
}
}
参考:数据结构–最小生成树详解