最小生成树_kruskal(贪心算法)

问题分析

首先复习以下最小生成树的相关概念:

生成树:一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。

最小生成树:当图中每条边都存在权重时,这时候我们从图中生成一棵树(n - 1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。

Kruskal算法是求解图中最小生成树的一个经典算法。算法思路如下:

1、寻找图中权重最小的边加入到最小生成树中,并将该边从图中删去;

2、若加入该边后生成树中构成了回路,则放弃该边,回到步骤1;

3、若图中所有边都浏览过或者最小生成树中已有(n-1)条边,则算法结束,否则回到步骤1。

Kruskal算法符合贪心算法的思想,每次都是选取当前图中权重最小的边进行比较选择,是比较好理解的。下面看一下简单的示例:

假设提供的图如下:

在这里插入图片描述

共有a,b,c,d四个顶点以及四条边,权重分别为1,2,5,6;

选取权重为1的边,无回路,保存该边;

在这里插入图片描述

选取权重为2的边,无回路,保存该边;

在这里插入图片描述

选取权重为5的边,出现回路,舍弃该边;
在这里插入图片描述

选取权重为6的边,不存在回路,保存该边;

在这里插入图片描述

图中每条边都浏览完毕,算法结束,最后得到的树就是最小生成树。

算法伪代码

struct Node
{
    int left;//边的一个顶点
    int right;//边的另一个顶点
    int weight;//权重
    friend bool operator<(const Node s1, const Node s2)
    {
        return s1.weight > s2.weight;//按照权重在优先队列中从小到大排列
    }
};

int n;//记录顶点数
int num=0;//记录最小生成树中边的个数
priority_queue open;//记录图中的各边信息
queue close;//记录保存在最小生成树中的各边信息

void Kruskal()
{
    while (open表中不为空)//浏览图中每一个边后停止
    {
        Node temp = open.top();//为浏览过的图的边中权重最小的边
        open.pop();
        if (现在选取的边加入到close中后使得“最小生成树”出现回路)
            舍弃该边,直接进入到下一次循环;
        将该边的信息放入到close表中;
        num++;
        if(num==n-1)
            最小生成树已经找到,退出while循环;
    }
    //最终close表中的信息就是构成最小生成树各边的信息
}

并查集的实现

在寻找最小生成树的过程中,每选择加入一条边,需要判断是否构成回路,如果构成回路,就去掉该边重新选取。我们可以直观的看出是否构成回路,但是代码又该如何实现呢?这与树和图的性质有关,针对本题我们引出并查集的相关知识。

定义:并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

主要构成:并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。

数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。

并查集原本的作用是用来计算图中有几个连通分量的数据结构,但是本题我们用的并不是这个功能,而是判断是否构成回路,所以下面举个例子来看一下如何利用并查集判断图中是否构成回路。

蓝色边代表还未插入到图中,下列表格记录的pre[ ]数组的信息。

i	        1	2	3	4	5	6	7
pre[ i ]	1	1	1	2	2	3	7

pre[ ] 记录了每个点的前驱节点是谁,1节点为老大,没有前驱节点,所以pre[ 1 ] = 1,7节点此时也是如此,2和3节点的前驱节点都是1,4和5的前驱节点都是2,6的前驱节点是3。

现在我们尝试往图中加入边first。first的相关节点是6和7,我们通过一层层判断6和7的最上级是谁,该过程由find(x)函数实现,两个节点最上级如果相同,那边加入此边后,图就会含有回路,反之没有回路。

证明如下:

如果find(a)=find(b),那么我们可以判断a节点和b节点是可达的,那么这时候比存在若干节点v,使得有条路连通a和b,这时我们再将ab边加入其中,该路就变成了回路。

回到我们的示例中,find(6)=1,find(7)=7,两者不等,我们就可以把边first加入到图中并且不会产生回路。

find()函数实现如下:

int find(int x)
{
    if (pre[x] == x)
        return x;
    else
        return pre[x] = find(pre[x]);
}

将边first加入到图中,实际上也就是两个连通分量的合并,意味着我们要对pre[ ]数组进行更新,将其中一个连通分量的老大设为另一个连通分量的小弟即可,至于到底谁是老大无所谓(这一点跟画出来的图或树会有所冲突,但是没有关系,我们能达到判断是否构成回路的目的即可),join()函数实现代码如下:

void join(int x, int y)
{
    pre[find(x)] = find(y);
}

更新后的pre[ ]数组如下

i	        1	2	3	4	5	6	7
pre[ i ]	7	1	1	2	2	3	7

再将second边放入到图中,可以通过find()函数得到find(5)=7,find(6)=7,所以不能将second边放入到图中。
以上就是对于并查集在本题中的相关应用了,在最开始别忘了对于pre[ ]数组进行初始化,此时一个节点就是一个连通分量,每个节点都是各自分量的老大,所以将设置pre[ i ] = i 就可以了。

完整代码

#include <iostream>
#include <queue>
using namespace std;
#define N 150
struct Node
{
    int left;
    int right;
    int weight;
    friend bool operator<(const Node s1, const Node s2)
    {
        return s1.weight > s2.weight;
    }
};

int n;
int num;
int M[N][N];
priority_queue<Node> open;
queue<Node> close;
int pre[N]; // 并查集数组,表示每个节点的父亲节点

void Init()
{
    int i, j;
    for (i = 1; i <= n; i++)
        for (j = i + 1; j <= n; j++)
        {
            if (M[i][j] == 0)
                continue;
            Node temp = {i, j, M[i][j]};
            open.push(temp);
        }
    for (int i = 1; i <= n; i++)
        pre[i] = i;
}

// 查找x所在连通分量的根节点
int find(int x)
{
    if (pre[x] == x)
        return x;
    else
        return pre[x] = find(pre[x]);
}


// 合并x和y所在的连通分量
void join(int x, int y)
{
    pre[find(x)] = find(y);
}

// 判断边(u,v)是否构成环
bool hasCircle(int u, int v)
{
    if (find(u) == find(v))
        return true;
    else
    {
        join(u, v);
        return false;
    }
}

void Kruskal()
{
    while (!open.empty())
    {
        Node temp = open.top();
        open.pop();
        if (hasCircle(temp.left, temp.right))
            continue;
        ++num;
        close.push(temp);
        if(num==n-1)
            break;
    }
}

void Print()
{
    while (!close.empty())
    {
        Node temp = close.front();
        close.pop();
        cout << temp.left << ' ' << temp.right << ' ' << temp.weight;
        cout << endl;
    }
}

int main()
{
    int i, j;
    cin >> n;
    for (i = 1; i <= n; i++)
        for (j = 1; j <= n; j++)
            cin >> M[i][j];
    Init();
    Kruskal();
    Print();
    system("Pause");
    return 0;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晕晕乎乎的KUN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值