【数据结构与算法】最小生成树和并查集

  在图论领域,最小生成树是一种较为常见的树形结构。一个有 n 个结点的某连通图的生成树是原图的较小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通性的最少的边。最小生成树可以使用Kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。在本篇文章中,将会对这Kruskal算法进行介绍。


并查集

  在介绍Kruskal算法之前,需要掌握“并查集”。
  在一些有n个元素的集合应用问题中,我们常常是在开始时让每个元素构成单元素的集合,然后按一定顺序将属于同一组的元素所在的集合进行合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
  简单来说,这是一种查询效率极高的算法,通过并查集的使用,可以极大减少重复查询的时间。对建立最小生成树有着重要的作用。

并查集是整型数组

  并查集并不是使用一个set,而是建立一个整型数组,通过下表及其对应的值建立的所属关系。我的习惯是使用father[N]表示。这里我尽量减少概念性的内容,主要以算法的代码实现为主。理论部分可以参考

并查集

  • 并查集的声明
#define N 100005

int father[N];

其中N表示节点的最大数量。


  • 并查集的初始化
void init()
{
	for(int i = 0; i < n; i++)
		father[i] = i;
}

  这个函数在每次进行对应操作前都要调用,进行father的初始化。初始化的时候,每一个节点 i 的集合都是数值和自己一样,表示自己单独是一个集合,没有进行任何合并。

  • 并查集的查找
int findx(int x)
{
	if(father[x] == x)
		return x;
	return findx(father[x]);
}

  这个函数的作用是查询编号为x的节点对应在哪个编号的集合里面。if条件那里与下面介绍的“合并”操作有关。简单来说,每一个集合里面一定有一个点的编号和集合的编号一致,只要我们找到当前x和哪个点是一个集合,就能确定x在哪个集合。下面将有例子说明。

  • 并查集的合并
void merge(int x, int y)
{
	int a = findx(x);
	int b = findx(y);
	if(a == b)
		return;
	father[a] = b;
}

  这个函数的作用是合并两个点(集合)。由于findx将x和y对应的 满足father[x] == x 的点找出来了,所以将x所在集合的“中心点”a移动到b中,这样在后续的查找时,(father[a] != a)而是(father[a] == b ),实现了两个集合的“合并”。


  至此,并查集的三大方式就介绍完毕。下面看一个例子。


假设有5个点。

x01234
father[x]01234

现在合并1和2,merge(1,2),并查集变为

x01234
father[x]02234

现在合并1和3,merge(1,3),并查集变为

x01234
father[x]02334

这样,在查找1的时候,就会递归查找到3,查找2的时候,也会递归查找到3。

  • 查找函数的改进

在搞清楚并查集的查找过程之后,可以进行以下改进

int findx(int x)
{
	return father[x] == x ? x : father[x] = findx(father[x]);
}

整体框架是?:语句,当不一致的时候,同样会递归调用,主要区别在于:在每一次递归调用完毕,都会将中间查找结果赋值给对应father[x],这样简化了后续递归调用次数。(还是需要仔细想一想的)


Kruskal算法

  建立最小生成树的算法之一。整体逻辑是将所有边按照权值大小进行升序排列,然后判断边的两个节点是否已经连接(属于同一个并查集),如果没有连接,那么就进行合并操作,并且将权值进行累加。
算法实现如下:

int kruskal()
{
	int res = 0;
	sort(node, node + n, [](Node a, Node b){return a.val < b.val; });
	for(int i = 0; i < n; i++)
	{
		if(findx(node[i].from) != findx(node[i].to))
		{
			merge(node[i].from, node[i].to);
			res += node[i].val;
		}
	}
	return res;
}

例题

【问题描述】

连接多个建筑

【输入形式】

办公区域分布图的顶点按照自然数(0,1,2,n-1)进行编号,从标准输入中首先输入两个正整数,分别表示线路图的顶点的数目和边的数目,然后在接下的行中输入每条边的信息,每条边占一行,具体形式如下:
<n> <e>
<id> <vi> <vj> <weight>

即顶点vi和vj之间边的权重是weight,边的编号是id。

【输出形式】

输出铺设光缆的最小用料数,然后另起一行输出需要铺设的边的id,并且输出的id值按照升序输出。

【样例输入】

6 10
1 0 1 600
2 0 2 100
3 0 3 500
4 1 2 500
5 2 3 500
6 1 4 300
7 2 4 600
8 2 5 400
9 3 5 200
10 4 5 600

【样例输出】

1500
2 4 6 8 9

【样例说明】

样例输入说明该分布图有6个顶点,10条边;顶点0和1之间有条边,边的编号为1,权重为600;顶点0和2之间有条边,权重为100,其它类推。其对应图如下:
在这里插入图片描述
经计算此图的最少用料是1500,可以使图连通,边的编号是2 4 6 8 9。其对应的最小生成树如下:
在这里插入图片描述

实现代码:

#include <bits/stdc++.h>
using namespace std;

typedef struct NODE
{
    int index;
    int from, to;
    int val;
} Node;

void init(int n, int m, int *&father, Node *&node)
{
    father = new int[n];
    node = new Node[m];
    for (int i = 0; i < n; i++)
        father[i] = i;
}

int findx(int x, int *&father)
{
    return father[x] = father[x] == x ? x : findx(father[x], father);
}

void merge(int x, int y, int *&father)
{
    int a = findx(x, father);
    int b = findx(y, father);
    if (a != b)
        father[a] = b;
}

inline bool connected(int a, int b, int *&father)
{
    return findx(a, father) == findx(b, father);
}

int kruskal(int m, Node *&node, int *&father, vector<int> &v)
{
    int res = 0;
    sort(node, node + m, [](Node a, Node b) { return a.val < b.val; });
    for (int i = 0; i < m; i++)
    {
        if (!connected(node[i].from, node[i].to, father))
        {
            merge(node[i].from, node[i].to, father);
            res += node[i].val;
            v.push_back((node[i].index));
        }
    }
    return res;
}

void del(int *&father, Node *&node, vector<int> &v)
{
    delete father;
    delete node;
    v.clear();
}

int main(int argc, char *argv[])
{
    int *father = nullptr;
    int n, m, res = 0;
    Node *node = nullptr;
    vector<int> v;

    cin >> n >> m;
    init(n, m, father, node);

    for (int i = 0; i < m; i++)
        cin >> node[i].index >> node[i].from >> node[i].to >> node[i].val;
    res = kruskal(m, node, father, v);

    cout << res << endl;
    sort(v.begin(), v.end());
    for (int i : v)
        cout << i << " ";

    del(father, node, v);
    father = nullptr;
    node = nullptr;

    return 0;
}

畅通工程

代码

#include <bits/stdc++.h>
using namespace std;

typedef struct NODE
{
    int from, to, val;
} Node;

int father[105];
int n, m;
Node *node;

void init()
{
    for (int i = 0; i <= n; i++)
        father[i] = i;
}

int findx(int x)
{
    return father[x] = father[x] == x ? x : findx(father[x]);
}

void merge(int x, int y)
{
    int a = findx(x);
    int b = findx(y);
    if (a != b)
        father[a] = b;
}

inline bool connected(int a, int b)
{
    return findx(a) == findx(b);
}

int kruskal()
{
    int res = 0;
    sort(node, node + m, [](Node &a, Node &b) { return a.val < b.val; });
    for (int i = 0; i < m; i++)
    {
        if (!connected(node[i].from, node[i].to))
        {
            merge(node[i].from, node[i].to);
            res += node[i].val;
        }
    }
    return res;
}

int main(int argc, char *argv[])
{
    cin.tie(NULL);
    cout.tie(NULL);
    ios::sync_with_stdio(false);

    while (cin >> n, n)
    {
        init();
        m = n * (n - 1) >> 1;
        node = new Node[m];
        for (int i = 0; i < m; i++)
            cin >> node[i].from >> node[i].to >> node[i].val;
        cout << kruskal() << endl;
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

A91A981E

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

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

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

打赏作者

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

抵扣说明:

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

余额充值