图的最小生成树

针对无向图,而且图必须联通。最小生成树是指保持图的联通性的基础上保留n-1条边,裁剪掉其它边,保证留下来的边的权值总和最小而且不可以存在环路(裁剪原则)。通常有Prim算法和Kruskal算法。其中Prim算法时间复杂度为O(n^2),适合稠密图;Kruskal算法时间复杂度为O(e*lne),与边的条数有关,适合稀疏图。当图的各个边权值不同时,最小生成树唯一


1、Prim算法
1).输入:一个加权连通图,其中顶点集合为V,边集合为E;
2).初始化:Vnew = {x},其中x为集合V中的任一节点(起点),Enew 为空;
3).重复下列操作,直到Vnew = V:
a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将点v加入集合Vnew中,将边<u, v>边加入集合Enew中;
4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。

#define MAX 1000 // 这个数表示无穷,大于所有边的权值即可
#define N 6      // 点的个数

int label[N]; // 标记数组,标记该点是否被加入到vnew中
              // 实际操作过程中可以没有vnew这个数组
int lowcost[N]; // 表示点到vnew的最小权值
int adjecent[N]; // 表示对应最小权值的vnew中的点

int edge[N-1][3]; // 记录最终结果边

void prim(int start) // 指定起点
{
    int i;
    for(i=0; i<N; i++) // 初始化label lowcost adjecent
    {
        label[i] = 0; // 初始为未标记状态0
        lowcost[i] = a[start][i]; // a为邻接矩阵
        // a[x][y]为MAX当x==y或者xy之间不是邻接时
        adjecent[i] = start;
    }
    label[start] = 1; // 标记起点

    int j, min, u;
    for(i=0; i<N-1; i++) // 循环标记N个点,起点已标,所以循环N-1
    {
        u = -1; // u min用来选取每次加入vnew的权值最小的点
        min = MAX;
        for(j=0; j<N; j++)
        {
            if(label[j] == 0 && lowcost[j] < min)
            {
                // 遍历所有点,当该点不在vnew中,而且改点权值较小
                // 记录该点,最终得到最小的点
                min = lowcost[j];
                u = j;
            }
        }

        if(u != -1)
        {
            // 找到符合条件的点, 记录选择的这条边
            edge[i][0] = u;
            edge[i][1] = adjecent[u];
            edge[i][2] = a[u][adjecent[u]];

            label[u] = 1; // 标记该点,加入vnew

            for(j=0; j<N; j++)
                if(label[j] == 0 && a[u][j] < lowcost[j])
                {
                    // 加入u点之后更新lowcost adjecent
                    lowcost[j] = a[u][j];
                    adjecent[j] = u;
                }
        }
    }
}

2、Kruskal算法
首先将图的n个顶点看成n个孤立的连通分支(n个孤立点)并将所有的边按权从小大排序。然后按照边权值递增顺序,如果加入边后存在环则这条边不加,直到形成连通图。(如果加入边的两个端点位于不同的连通支,那么这条边可以顺利加入而不会形成环)

#define E 10 // 定义边数
#define N 6 // 定义点数

int edge[E][3]={{起点,终点,权重},{后面省略}}; // 表示所有边
int label[N] = {0}; // 标记点是否在最小生成树中,0为不在

int result[N-1][3]; // 结果最小生成树边

void kruskal()
{
    int i, j, tmp;
    // 按从小到大排序edge,排序方法可任选
    for(i=0; i<E-1; i++)
        for(j=i+1; j<E; j++)
            if(edge[i][2] > edge[j][2])
            {
                // 交换起点
                tmp = edge[i][0];
                edge[i][0] = edge[j][0];
                edge[j][0] = tmp;
                // 交换终点
                tmp = edge[i][1];
                edge[i][1] = edge[j][1];
                edge[j][1] = tmp;
                // 交换权重
                tmp = edge[i][2];
                edge[i][2] = edge[j][2];
                edge[j][2] = tmp;
            }

    // 遍历edge,保留所有符合条件的边(无环)
    int index = 0; // 记录result值时使用的变量
    for(i=0; i<E; i++)
    {
        // 如果这条边的起点终点均被标记,将会产生环路,放弃该边
        if(label[edge[i][0]] + label[edge[i][1]] == 2)
            continue;
        // 满足条件,则标记起点终点,并将该边加入到result中
        label[edge[i][0]] = 1;
        label[edge[i][1]] = 1;
        result[index][0] = edge[i][0];
        result[index][1] = edge[i][1];
        result[index][2] = edge[i][2];
        index ++;
    }
}

3、对2的修正。(不删除2的原因以为前车之鉴)
2的逻辑有遗漏。主要遗漏位置为

// 如果这条边的起点终点均被标记,将会产生环路,放弃该边
if(label[edge[i][0]] + label[edge[i][1]] == 2)
    continue;

这句话。本意是如果这条边的两个顶点如果都被标记那么就不选这条边,因为会产生环。后来发现这是不对的。比如0 1 2 3四个点。边<0,1>权值为1,边<2,3>权值为2,边<0,2>权值为4,边<1,3>权值为3。正确的最小生成树应该是<0,1><2,3><1,3>。而如果按照2的判断条件将会先标记0 1再标记2 3然后选择<1,3>这条边时由于1 3都被标记,这条边将会被遗漏。因此当图的点过多,那么可能遗漏的边就越多。将永远不会得到正确答案。

修正思路:最小生成树的中标记的点的个数在任何阶段总是大于选择的边数的。(除了开始一个点都没有标记,一条边都没有选择的情况,那时均为0)。如果它们出现了相等的情况,那么将产生环路。

改正后的代码:

int getLabelNum() //从标记数据中获取已经标记的点的个数
{
    int i, count = 0;
    for(i=0; i<N; i++)
    {
        if(label[i] == 1)
            count++;
    }
    return count;
}

if(index!=0 && index==getLabelNum())//index表示边数并排除没边的情况
    continue;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值