并查集
并查集是这样一种数据结构,它提供查询和合并的功能,所以叫并查集对吧。 具体什么意思呢? 可以把并查集抽象成很多个互不相交的集合组成的集合。
那么查询指的就是选择两个元素,查询它们是否在同一个子集合里面。我们举个栗子,({}表示集合),{{1,3},{2,4}} 假设我们查询操作是find(int x1,int x2)
,那么find(1,2)
和find(1,3)
分别返回假和真,因为很明显,1和3在同一个集合中,而1和2在不同的集合中。
而合并呢?就是让两个元素所属的集合合并为一个集合,就拿上面那个栗子来说吧,假设我们的合并操作是unite(int x,int y)
,那么unite(1,2)
的结果就是让原本的大集合变成了{{1,3,2,4}},两个集合变成了一个集合。
接着我们来思考一下怎么用代码实现并查集,不妨先试一试最正常的数组,如下:
i | 1 | 2 | 3 | 4 | 5 |
set[i] | 1 | 2 | 3 | 1 | 2 |
我们规定:set[i]表示所处集合,i表示该集合包含元素,照此,该例中有三个集合,1集合为{1,4},2集合为{2,5},3集合为{3}.
不难理解,查询操作的代码则可以如此编写:
int find(int x)
{
return set[x];//set[i] 代表 i所处的集合,返回x所处的集合
}
int same(int x,int y)
{
return find(x) == find(y);
}
合并操作的代码如下:
void unite(int x,int y,int n)//n代表一共有多少个元素
{
int i;
for(i = 0;i < n;i++){
if(set[i] == y)
set[i] = find(x); //把y所处的集合名改成x的达到合并的目的
}
}
但是,使用线性结构的数组是否能满足高效的特点呢?显然不能,于是,我们又想到了使用树结构,这时我们规定:
- i != set[i]时,set[i]是i的父亲
- i == set[i]时,i就是第i集合的根节点
再如下表:
i | 1 | 2 | 3 | 4 | 5 |
set[i] | 1 | 2 | 3 | 1 | 2 |
就可以看成三个集合,分别为{1->4},{2->5},{3}.
那么根节点就是区分不同集合的关键字,显然,获取所处集合的方法就是从元素出发,向上找到根节点,而合并就是把两棵树变成一棵树,为了更加高效,我们规定一种优化(路径压缩)的原则:
- 深度小的树连在深度大的树根节点上
- 让元素直接连在根节点上
代码如下(分析在注释中):
int set[MAX];//代表父亲的集合
int depth[MAX];//代表树的深度
int find(int x) //获取根节点值
{
if(x == set[x])
return x;
else
return set[x] = find(set[x]);//实现原则2(让元素直接与根节点相连)、同时沿着父亲向根节点递归进发
}
void unite(int x,int y) //合并x、y所处的集合
{
x = find(x);y = find(y); //找到x、y所处的集合
if(x == y) return ; //如果是相同集合,则不需要合并,直接返回
if(depth(x) < depth(y)) //如果x比y深度小,把x接到y上面,直接让y成为x的父亲即可
{
set[x] = y;
}
else
{
set[y] = x; //反之,把y接到x上
if(depth(x) == depth(y)) depth(x)++;//如果两棵树深度相同,则合并之后它们的深度加一
}
}
int same(int x,int y) //判断x、y是否在同一个集合中
{
return find(x) == find(y);
}