概念
在一给定的无向图 G = ( V , E ) G=(V,E) G=(V,E) 中, ( u , v ) (u,v) (u,v) 代表连接顶点 u u u 与顶点 v v v 的边,而 w ( u , v ) w(u,v) w(u,v) 代表此边的权重,若存在 T T T 为 E E E 的子集且为无循环图,使得联通所有结点的的 w ( T ) w(T) w(T) 最小,则此 T T T 为 G G G 的最小生成树。
即 w ( t ) = ∑ ( u , v ) ∈ t w ( u , v ) w(t)=\sum_{(u,v)\in t}w(u,v) w(t)=∑(u,v)∈tw(u,v)
最小生成树其实是最小权重生成树的简称。
以上是百度百科对最小生成树的定义,我相信大家肯定是看不懂的了,我给大家解释得通俗易懂一下:
定义:最小生成树,又称“MST”,即把连通图中的所有结点全部连接起来的最小路径和,只能连 n − 1 n-1 n−1 条边, n n n 是顶点的数量。
比如,下面这个图中用蓝色的边所构成的树就是一个最小生成树。
我们可以见到,这个连通图中的每一个点都在这个最小生成树中的。此时这个最小生成树的边权之和为 ( 2 , 4 ) + ( 4 , 5 ) + ( 3 , 5 ) + ( 1 , 5 ) + ( 5 , 6 ) + ( 6 , 7 ) = 1 + 4 + 3 + 2 + 2 + 3 = 15 (2,4)+(4,5)+(3,5)+(1,5)+(5,6)+(6,7)=1+4+3+2+2+3=15 (2,4)+(4,5)+(3,5)+(1,5)+(5,6)+(6,7)=1+4+3+2+2+3=15。
知道了最小生成树是什么,那么我们怎么求最小生成树呢?这里就要介绍到两个算法Prim算法和Kruskal算法。
求最小生成树
Prim算法
思想
Prim算法本质上是由贪心来实现的,和dijkstra算法没有本质上的区别,Prim算法就是把所有目前已经可以是这个最小生成树的点和边合并在一个集合里。
那么Prim算法的具体思路又是什么呢?我们根据上面这个实例继续示范。
在熟悉Prim算法之前,我们先得懂一个概念点到集合的距离。
所谓点到集合的距离,即一个点到这个集合里面所有点的距离的最小值。
第一步,我们先从 1 1 1 号点开始(选什么点开始都无所谓),此时我们把 1 1 1 号点装进集合里。
接着,我们遍历一下与集合里的点相邻且不在集合内的点。求出这个点到集合的距离,取最小值,如果这个点不能够通往集合,那么这个点到集合的距离就是正无穷。
因为距离
1
1
1 号点最近的点就是
5
5
5 号点了,
5
5
5 号点到集合的距离为
2
2
2。我们把
5
5
5 号点加入集合。
接着,我们找里集合最近的点,经过遍历,我们得知编号为 6 6 6 的点到集合的距离为 2 2 2,是最短的,于是编号为 6 6 6 的点进入集合。
接着往下以此类推。
到了这一步,大家都应该会想了,此时有
2
2
2 个还没有进入最小生成树的边的边权是
4
4
4,就是最小的,这个时候应该选谁呢?
很明显啊,应该选
(
5
,
4
)
(5,4)
(5,4) 这一条边,因为虽然
(
1
,
7
)
(1,7)
(1,7) 这一条边还不是最小生成树里的边,可是
1
1
1 和
7
7
7 两个点都已经在集合里面了,所以连不了。
如果这两条边都符合要求,那么随便连哪一条都可以。
根据以上规律类推,可以知道最后该图的最小生成树为:
用Prim算法来求最小生成树的思路大家都懂了吧。大家也许会惊奇的发现,Prim算法的思想和dijkstra算法的思想好相似,其实代码打起来也挺相似的。唯一的区别就在于dijkstra算法的思想是求点到点的距离,而Prim算法的思想是求点到集合的距离。
懂了思想,代码就应该很好打了吧。
Code
int g[1005][1005], n; // n表示点数,g是邻接矩阵,存储所有边
int dist[1005]; // 存储其他点到当前最小生成树的距离
bool st[1005]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim() {
memset(dist, 0x3f, sizeof dist);
int res = 0;
for (int i = 0; i < n; i ++ ) {
int t = -1;
for (int j = 1; j <= n; j ++ ) {
if (!st[j] && (t == -1 || dist[t] > dist[j])) {
t = j;
}
}
if (i && dist[t] == INF) {
return INF;//INF是无穷大,即0x3f
}
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) {
dist[j] = min(dist[j], g[t][j]);
}
}
return res;
}
Kruskal算法
Kruskal算法可以说是一种十分玄学的一种算法,主要用于稀疏图中,很容易懂,但是证明起来有点难。
思想
Kruskal算法的具体思路很简单,就是把一个图的每一条边按照权重排序,每次权重最小的边进入最小生成树,前提是这条边的两个端点至少有一个不在生成树中,Kruskal简单,好理解,就在于我刚刚阐释的这个思路估计大家都心知肚明,下面我们就来具体讲一讲Kruskal的实现过程。
第一步,我们先将这个图中的边按照边权从小到大排序,选择最小的那一条边,把这一条边和它的两个节点进入集合。
我们可以看到, ( 2 , 4 ) (2,4) (2,4) 这一条边在最小生成树中, 2 2 2 号点和 4 4 4 号点都进入了集合。
接下来的步骤以此类推。
注意: \color{red}{注意:} 注意: 当这一条边的两个节点都已经集合中了,那么这一条边应当舍去(因为取了这一条边该最小生成树就成了环,不符合树的定义了)
这时,大家都应该会有一个问题:怎么判断这一个点是否在集合里呢?
并查集是一个好用的东西,我们只需要把符合要求的点合并在一个集里,用一个 a n s ans ans 变量记录一下该边的边权即可。
实现
kruskal算法实现起来比较简单,代码也很短,也比较好理解。这里就直接贴代码了。
int n, m; // n是点数,m是边数
int p[1000005]; // 并查集的父节点数组
struct Edge { // 存储边
int a, b, w;
bool operator< (const Edge &W)const {
return w < W.w;
}
} edges[1000005];
int find(int x) { // 并查集核心操作
if (p[x] == x) {
return x;
}
return p[x] = find(p[x]);
}
int kruskal() {
sort(edges, edges + m);
for (int i = 1; i <= n; i++) {
p[i] = i; // 初始化并查集
}
int res = 0, cnt = 0;
for (int i = 0; i < m; i++) {
int a = edges[i].a;
int b = edges[i].b;
int w = edges[i].w;
a = find(a), b = find(b);
if (a != b) { // 如果两个连通块不连通,则将这两个连通块合并
p[a] = b;
res += w;
cnt ++;
}
}
if (cnt < n - 1){
return INF;
}
return res;
}