并查集(disjoint-set forests)

原文地址:http://blog.csdn.net/ariesjzj/article/details/8001597


举个简单应用的例子。现在社交网站这么流行,假设现在想知道两个人之间是否存在间接好友关系(A和B为好友,B和C为好友,A和C为间接好友),有什么好方法呢?并查集就是用于这类查询问题的有效数据结构,正如其名(disjoint set),并查集本质上是一个集合,集合的元素为树,因此并查集实际上表示了一个森林(disjoint-set forests)。它的特点是每棵树中的成员都可由根结点所代表,这样要知道两个结点是否属于集合的同一元素,只要看它们是否有同一“代表”。

 

逻辑很简单,事实上原始的实现会比较慢,因为这儿的树并不保证平衡,极端情况下树会变成“条形”(每个结点只有一个孩子)。一般,我们会使用两种优化:称为union by rank和path compression。前者使集合在合并过程中尽可能平衡,后者使每个结点直接指向根结点,因此使查询在常数时间完成。根据这两种优化,下面是一个教科书式的实现:

int rank[100];
int p[100];

void makeSet(int x)
{
	p[x] = x;
	rank[x] = 0;
}

void unionSet(int x, int y)
{
	linkSet(findSet(x), findSet(y));
}

void linkSet(int x, int y)
{
	if (rank[x] > rank[y])  //union by rank
		p[y] = x;
	else {
		p[x] = y;
		if (rank[x] == rank[y])
			rank[y] = rank[y] + 1;
	}
}

int findSet(int x) {
	if (x != p[x])  // path compression
		p[x] = findSet(p[x]);
	return p[x];
}

 

古人云,光说不练假把式,我们来看看这个看似简单的数据结构能解决点什么实际问题:

RoadReconstruction(Single Round Match 356 Round 1 - Division II, Level Three)

题目并不诡异,给出一些城市间的道路,有些是好的有些是破的,如果是破的还给出修的代价,问如何以最少的代价使所有城市全连通。咋一看是图论的最小生成树问题,其实。。。的确可以用最小生成树解,但这里我们尝试用并查集来解。注意解中利用了以下事实:初始时集合中有n棵树,每个树只有一个结点,自己“代表”自己。经过m轮合并,变成n-m个,经过n-1轮,合并为一个,则原森林中的所有结点全连通。

#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <algorithm>
#include <string>
#include <set>
#include <map>

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;

typedef struct damaged_node {
	int cost;
	string id;
	int city1;		
	int city2;
} damaged_node;

typedef struct non_damaged_node {
	int city1;
	int city2;
} non_damaged_node;

bool compare(const damaged_node& r1, const damaged_node& r2)
{
	return r1.cost < r2.cost;
}

int rank[100];
int p[100];


class RoadReconstruction {
public:
	void makeSet(int x)
	{
		p[x] = x;
		rank[x] = 0;
	}

	void unionSet(int x, int y)
	{
		linkSet(findSet(x), findSet(y));
	}

	void linkSet(int x, int y)
	{
		if (rank[x] > rank[y])
			p[y] = x;
		else {
			p[x] = y;
			if (rank[x] == rank[y])
				rank[y] = rank[y] + 1;
		}
	}

	int findSet(int x) {
		if (x != p[x])
			p[x] = findSet(p[x]);
		return p[x];
	}

	string selectReconstruction (vector <string> roads) 
	{
		int n = roads.size();
		int i;
		int cno = 0;

		map<string, int> m;	// map city name to no. in disjoint set
		vector<damaged_node> damaged;
		vector<non_damaged_node> non_damaged;
		vector<string> res;
		string ret = "";

		// parse the arguments
		for (i = 0; i < n; ++i) {
			string id, city1, city2;
			int cost = 0;
			istringstream iss(roads[i]);
			iss >> id >> city1 >> city2 >> cost;
			if (m.find(city1) == m.end())
				m[city1] = cno++;	// generate unique id for each city
			if (m.find(city2) == m.end())
				m[city2] = cno++;

			if (!cost)	{  // no cost, non damaged roads
				non_damaged_node t = {m[city1], m[city2]};
				non_damaged.push_back(t);
			} else {	// damaged roads
				damaged_node t = {cost, id, m[city1], m[city2]};
				damaged.push_back(t);
			}
		}

		// init the disjoint set
		for (i = 0; i < cno; ++i) {
			makeSet(i);
		}

		int cnt = 0;

		// reduce the disjoint set by considering non-damaged roads
		for (i = 0; i < non_damaged.size(); ++i) {
			int city1_no = non_damaged[i].city1;
			int city2_no = non_damaged[i].city2;
			findSet(city1_no);	// update the parent, directly point to the representative
			findSet(city2_no);
			if (p[city1_no] != p[city2_no]) {
				unionSet(city1_no, city2_no);
				cnt++;
			}
		}

		// try to reconstruct the damaged roads, in increasing order of cost
		sort(damaged.begin(), damaged.end(), compare);
		for (i = 0; i < damaged.size(); ++i) {
			int city1_no = damaged[i].city1;
			int city2_no = damaged[i].city2;
			findSet(city1_no);
			findSet(city2_no);
			if (p[city1_no] != p[city2_no]) {
				unionSet(city1_no, city2_no);
				res.push_back(damaged[i].id);
				cnt++;
			}
		}

		if (cnt != cno - 1)		
			return "IMPOSSIBLE";
		else {	// all citys were merged into one.
			sort(res.begin(), res.end());
			for (i = 0; i < res.size(); ++i) {
				if (i)
					ret += " ";
				ret += res[i];
			}
			return ret;
		}
	}
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值