并查集的实现

上一篇实现kruskal算法用到了并查集,并大概自说自话地讲了并查集的原理。即只关心是否属于一个集合这个属性,不关心元素之间的关系(并查集看着有根树,肯定是连通且无环的),即不关心它们是怎么连接的。


通常

优化并查集的查询,用到了一种叫做路径压缩的技术。这个听起来很高端,其实没什么。如果觉得递归版本不好理解,就使用迭代版本。

优化并查集的合并,用到了一种叫做按秩合并的做法,用一个rank数组来记录代表元素子树的最大可能高度,把矮树直接挂到高树下作为其直接孩子。


以poj1611 http://poj.org/problem?id=1611 为例


借助POJ,我们还可以比较一下,只优化查询和优化查询+优化合并、迭代与递归的路径压缩三个版本的时间效率


#include <iostream>
using namespace std;

int const N = 40000;
int par[N];
int cnt[N]; //POJ1611需要

//创建并查集
void makeSet(int n)
{
	for (int i = 0; i != n; i++) {
		par[i] = i;
		cnt[i] = 1;
	}
}

//查询
//即查找集合的代表元素(查找树根)
//路径压缩采用递归的方法(递归实现简洁、常见)
//POJ1611 仅递归路径压缩优化 耗时 141MS
int getPar(int i)
{
	if (par[i] != i) {
		par[i] = getPar(par[i]);
	}
	return par[i];
}

//_查询
//路径压缩采用迭代的方法
//POJ1611 仅迭代路径压缩优化 耗时 157MS
int _getPar(int i)
{
	//如果是直接孩子或树根,则直接返回树根
	if (par[i] == i)
		return par[i];
	
	int rec[N], root, k = 0;
	//退出循环时即找到了树根i,因为此时par[i] == i;
	//查找路径上的结点已经保存到rec数组,树根保存为root
	//数组中的元素其父亲全部设置为root
	for (; par[i] != i; i = par[i])
		rec[k++] = i;
	root = i;
	for (i = 0; i != k; i++)
		par[rec[i]] = root;
	return root;
}

//简单合并
bool unionSet(int far, int son)
{
	int a = getPar(far);
	int b = getPar(son);
	if (a == b) //判断是否属于同一个集合
		return false; //合并失败:标志着它们属于同一个集合
	par[b] = a;
	cnt[a] += cnt[b];
	return true; //合并成功:标志着它们原本不属于同一个集合,现已经合并为同一个集合
}

//_按秩合并
//这里秩是指每个结点的子树的最大可能高度(刚开始子树空,rank初始为0)
//画画图就可以理解秩的含义了
//POJ1611 递归路径压缩优化+按秩合并优化 耗时 125MS
int rank[N] = {0};
bool _unionSet(int x, int y)
{
	int a = getPar(x);
	int b = getPar(y);
	if (a == b) //判断是否属于同一个集合
		return false;

	if (rank[a] > rank[b]) {
		par[b] = a; //a为新树根
		cnt[a] += cnt[b];
	}
	else {
		par[a] = b; //b为新树根
		cnt[b] += cnt[a];
		if (rank[a] == rank[b])
			rank[b]++; //b的子树的秩加1
	}
	return true;
}

int main()
{
	int n, m;

	while (cin >> n >> m, n||m) {
		makeSet(n);
		for (int i = 0; i != m; i++) {
			int t, first, cur;
			cin >> t >> first;
			for (int j = 1; j != t; j++) {
				cin >> cur;
				_unionSet(first, cur);
			}
		}
		cout << cnt[getPar(0)] << endl;
	}
	return 0;
}

实际情况下,有路径压缩的并查集已经足够高效了,不需要按秩合并。

另外,并查集的题目往往涉及大量数据的读入,一般用scanf()更快。

最优版本改成scanf(),耗时则从125MS降到16MS。

据说还有一种方法,就是继续用cin cout,但在主函数内第一行加入一行代码:

ios::sync_with_stdio(NULL);

实测发现速度是47MS,看来还是不如scanf() printf()快。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值