一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 [1] 最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。
例如:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
说明
最小生成树性质:设G=(V,E)是一个连通网络,U是顶点集V的一个非空真子集。若(u,v)是G中一条“一个端点在U中(例如:u∈U),另一个端点不在U中的边(例如:v∈V-U),且(u,v)具有最小权值,则一定存在G的一棵最小生成树包括此边(u,v)。
证明(引用百度)
为方便说明,先作以下约定:
①将集合U中的顶点看作是红色顶点,②而V-U中的顶点看作是蓝色顶点,③连接红点和蓝点的边看作是紫色边,④权最小的紫边称为轻边(即权重最"轻"的边)。于是,MST性质中所述的边(u,v)就可简称为轻边。
用反证法证明MST性质:
假设G中任何一棵MST都不含轻边(u,v)。则若T为G的任意一棵MST,那么它不含此轻边。
根据树的定义,则T中必有一条从红点u到蓝点v的路径P,且P上必有一条紫边(u',v')连接红点集和蓝点集,否则u和v不连通。当把轻边(u,v)加入树T时,该轻边和P必构成了一个回路。删去紫边(u',v')后回路亦消除,由此可得另一生成树T'。
T'和T的差别仅在于T'用轻边(u,v)取代了T中权重可能更大的紫边(u',v')。因为w(u,v)≤w(u',v'),所以
w(T')=w(T)+w(u,v)-w(u',v')≤w(T)
即T'是一棵比T更优的MST,所以T不是G的MST,这与假设矛盾。
所以,MST性质成立。 [1]
说简单点: n个顶点,组成的最小生成树n-1条边,连接着这n个顶点,使得任意两个点之间都能到达且选出n-1条边和最小的方案(不含环),把n个顶点想成n颗树,最终把n个顶点连成一棵树,每次取权值最小的边(所以第一步是给所有的边按从小到大排序),判断是否在属于一棵树,属于则pass,不属于则选中,如此下去直到所有的点都在一棵树上即完成了这颗最小生成树
难点重点剖析:判断是否属于一棵树可以使用状态压缩(并查集),效率高
附上伪代码:
//所有的边排序
//初始化MST为空
//初始化连通分量,使每个点各自成为一个独立的连通分量
for (int i = 0; i < m; i++) {
if (e[i].u和e[i].v不在同一连通分量) {
把边e[i]加入MST
合并e[i].u和e[i].v所在的连通分量
}
}
分析上图配合伪代码,1 2 3 4 5 6都各自是一棵树,选取最下的边1 3连成的边权值为1,判断1 3不属于一棵树,满足条件
选定1 3连成的边,把1 3设置为一棵树,继续选定6 4组成的边权值2同理,同理2 5 直到满足n-1条边。
上面的伪代码,最关键的地方在于“连通分量的查询和合并”,需要知道任意两个点是否在同一连通分量中,还需要合并两个连通分量。这个问题正好可以用并查集完美的解决。
并查集(Union-Find set):
初始状态每个点都是一个树,n个点是n颗树组成的森林,每次操作需要搜索一条边的两个点是否属于一棵树,不是就需要合并,是的话就舍弃,不然会出现环,并查集就是充当着查与并的功能,状态压缩就是树只有一个根,根有儿子结点,没有孙子结点,这样在查的时候就非常的快。
附上并查集代码:
int parent[1002];
void init(int n)//初始化
{
for(int i=1;i<=n;i++) {
parent[i]=i;
}
}
int find(int x)//寻找根节点
{
return parent[x]==x?x:find(parent[x]);//状态压缩,只有结点和儿子结点这两层关系
}
void unite(int x,int y)//连接,分集合
{
x=find(x);
y=find(y);
if(x==y) {
return ;
}
else {
parent[x]=y;
}
}
附上克鲁斯卡尔算法代码:
//Kruskal
int pRoot[1000], pResult, N, M;
struct Node{
int pStart, pEnd, val;
}edge[5000];
bool cmp(Node tmp_a, Node tmp_b)
{
return tmp_a.val < tmp_b.val;
}
void init()
{
scanf("%d%d", &N, &M);
for (int i = 0; i < N; i++) {
pRoot[i] = i;
}
for (int i = 0; i < M; i++) {
scanf("%d%d%d", &edge[i].pStart, &edge[i].pEnd, &edge[i].val);
}
sort(edge,edge+M,cmp);
}
int Find(int x)
{
return pRoot[x] == x?x:Find(pRoot[x]);
}
void Kruskal()
{
int tmp_M = 0;
for (int i = 0; i < M; i++) {
if(tmp_M == N - 1) {
break;
}
int find_a = Find(edge[i].pStart);
int find_b = Find(edge[i].pEnd);
if (find_a != find_b) {
pRoot[find_a] = find_b;
tmp_M++;
pResult += edge[i].val;
}
}
}
int main()
{
init();
Kruskal();
printf("%d\n", pResult);
}
以上题目,N个点,M条边求最小生成树
用上诉图数据进行测试
输入:
1 2 6
1 3 1
1 4 5
2 3 5
2 5 3
3 4 5
3 5 6
3 6 4
4 6 2
5 6 6
输出:15