最小生成树(MST)
我们知道,有n个结点,n-1条边的连通图是一棵树,也就是说想连通n个结点,至少需要n-1条边。在实际问题中可能每条边的 “费用”不一样,这样我们想用最小的 “花费”,连接这n个顶点,这是最小生成树研究的问题。
最小生成树是在无向图上研究的。
Prim算法
在学习Prim算法之前,最好能了解Dijkstra算法,在我另一篇博客里,因为二者的贪心思想非常相似,如果能理解Dijkstra,学习Prim会非常容易。
正题:
解释Prim算法,我准备用和Dijkstra算法一样的 “蓝白点” 思想,只不过蓝点和白点和含义变了:白点表示已经在生成树中(已经被连通)的点,蓝点表示目前没有被联通的点。初始时所有点都没有连通,都是蓝点。
和Dijkstra算法一样,我们也定义一个 dis[] 数组,也是和Dijkstra的dis[],含义不同,这里的 dis[u],表示蓝点u和它所直接相连的白点的最短边的长度,或者叫最小边权的大小。**初始时所有 dis[] = INF,因为所有点都是蓝点,所以没有和蓝点直接相连的白点。**生成树总得有一个起点,我们人为规定一个起点,用这个起点去联通其他顶点,令起点的 dis[] = 0,我这里用1号点为起点,dis[1] = 0。然后开始算法。
先找到所有 dis[u] 中最小的一个u,然后把u从蓝点变成白点,同时把此时dis[u] 的长度累加到最小生成树的总权值里,然后用这个新的白点u,修改与u直接相连的蓝点的 dis[]。
这套操作是不是似曾相识?
这和Dijkstra的流程很像啊
原因就是:两个算法的贪心策略基本一致。
Dijkstra的贪心策略是如何成立的?
这是在Dijkstra那篇里截的。在Prim算法里,有一句和红线画出来的话相似的一句:任何时候你在 dis[u] 中找到一个最小的u,这个 dis[u] 所表示的那条边一定在最小生成树里。
我还是用图片解释这句话。
左边表示所有白点的集合,右边表示所有蓝点的集合,我把白点和蓝点间的边抽象为两个集合之间的边。从上图可以看出,两个集合间的边,有一条长度为 1 是最小的,用红线标出来。按照前面的说法,这条边应该在最小生成树里。现在,我 假设 这条边不在最小生成树中,而是另一条绿色的边在最小生成树里,看看会发生什么。
我们知道最小生成树最后要连通所有结点,也就是蓝点都要变成白点,那么现在 a2 进入了最小生成树,a1 没有。那么a1 再想进入最小生成树,有两种方法:
- 两个集合之间还有除红色边以外的边,连接某个白点和 a1。
- 通过 a2 可以从蓝点集内部连接 a2 和 a1 ,这样就能在后续的处理中,通过a2和其他若干结点连接 a1。
很显然对于第一种情况,红色边已经是最短的边了,即使还有其他边连接某个白点和 a1 它不会比红边更短,所以不如直接让红边进入最小生成树。这个很好理解。关键看第二种情况。
其实,第二种情况也可以用一句话否决。
“能连过来,一定能连回去”。
综上所述,任何时候你在 dis[u] 中找到一个最小的u,这个 dis[u] 所表示的那条边一定在最小生成树里。
到现在,就解释完了Prim算法的核心,他的贪心思想。
下面补几张图模拟一下算法,然后放代码。
memset(dis, 0x7f, sizeof(dis));
dis[1] = 0;
for(int i=1;i<=N;i++)
{
int minn = 2147483647, index;
for(int j=1;j<=N;j++)
{
if(dis[j]<minn&&vis[j]==0) //寻找dis[j]最小的蓝点
{
minn = dis[j];
index = j;
}
}
vis[index] = 1;
MST += dis[index];
for(int j=1;j<=N;j++)
{
if(vis[j]==0&&map[index][j]!=0)
if(map[index][j]<dis[j])
dis[j] = map[index][j];
}
}
printf("%d\n", MST);
Kruskal算法
中文名克鲁斯卡尔算法,学习这个算法有一个前置知识( 并查集连接),在我的另一篇博客里。哦对了,还要会快速排序,会用C++的 sort() 就行,不用了解快排原理。
我们把无向图中相互连通的结点称为连通块,Kruskal将一个连通块当成一个集合,首先将所有的边按长度从小到大排序,然后按顺序枚举边,如果这条边连接着两个集合,就把这条边加入最小生成树,同时把两个集合合并。否则就跳过这条边。n个集合变成一个集合显然要合并 n-1 次,这样就选出了最小生成树的n-1条边。
由于边是按从小到大枚举的,保证了生成树的最小。
Kruskal算法思维难度较低,只要理解了并查集,理解Kruskal并不难,关键在于怎么实现。由于实现Kruskal需要熟练使用邻接表储存图和 sort() 排序,这些不在本文的范围内,所以我准备把标准Kruskal完整代码发出来,有兴趣的同学自己研究吧。
#include <cstdio>
#include <algorithm>
using namespace std;
struct t_Edge {
int next;
int from, to;
int dis;
};
t_Edge edge[20001];
int N, M, a, b, c, MST, cnt;
int head[2010], num_edge;
int father[2010];
bool cmp(t_Edge a, t_Edge b)
{
return a.dis<b.dis;
}
void add(int from, int to, int dis)
{
edge[++num_edge].next = head[from];
edge[num_edge].from = from;
edge[num_edge].to = to;
edge[num_edge].dis = dis;
head[from] = num_edge;
}
int find(int x)
{
if(father[x]!=x)
return find(father[x]);
return father[x];
}
int main()
{
scanf("%d%d", &N, &M);
for(int i=1;i<=N;i++)
father[i] = i;
for(int i=1;i<=M;i++)
{
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
add(b, a, c);
}
sort(edge+1, edge+num_edge+1, cmp);
for(int i=1;i<=num_edge;i++)
{
int u = edge[i].from, v = edge[i].to;
int r1 = find(u), r2 = find(v);
if(r1!=r2)
{
cnt++;
MST += edge[i].dis;
father[r2] = r1;
if(cnt==N-1)
break;
}
}
printf("%d\n", MST);
return 0;
}
涉及到本文知识的题目
模板: 洛谷P3366 【模板】最小生成树.
最小生成树 + 思维: 洛谷P2212 USACO14MARWatering the Fields S.
最小生成树 + 思维: 洛谷P1991 无线通讯网.