最小生成树(Kruskal)算法

定义:

     一个有 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]

 

Kruskal算法简述:

   说简单点: 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所在的连通分量 
    }
}

    最小生成树(Kruskal)算法_第1张图片

   

    分析上图配合伪代码,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

你可能感兴趣的:(算法,数据结构)