并查集与路径压缩

引子
现在来看这样一个经典问题:
亲戚
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

怎么做?
深搜?广搜?效率太低。
邻接矩阵?哇MLE(爆内存)!
于是我们有一种新的方法——并查集

分析一下这道题,我们发现题目的核心在于判断两个元素的关系(是否处于一个集合),输入给出n对元素两两相连。看来重点在于如何建出整个集合(可能是多个)来便于查询


并查集的作用判断两个集合(点)之间的关系

并查集的结构每一个集合中的元素指向那个集合的代表元素(father)。如此当判断A和B是否在一个集合中的时候,我们只需要查询他们的father是否是同一个便可得出结论(后来有些变动,但大致意思是这样的)。

并查集的初始化
有n个元素,开始我们将它的father 指向它自己,也就是说每个集合中只有一个元素。

并查集的合并
现在我们得知A和B是亲戚,那么我们如何合并这两个集合哪?
答案是找到他们各自的father将其中一个father指向另一个(father[fathera] = fatherb),这样我们便完成了并查集的合并。

那么出现了一个问题,请看下例:
已知fa[1] = 2, fa[2] = 3, fa[3] = 4,现在我们得知2和6有关系,那么根据上述步骤fa[2] = 6,如此完成了合并。fa (father)
我现在想知道2和4是否有关系,判断得到fa[2] != fa[4],没有关系!但事实上…
看来我们缺少了一个步骤——将该集合的其他元素的fa也更新为新的father。
由此便引出了并查集的核心之一——代表元素的选择


根据上文,我们知道每一个集合的元素都指向该集合的代表元素,那么该代表元素可以随意取吗?
很显然,自然是不可以。那么代表元素需要有些什么要求?
根据fa[i] = j得出i的父亲是j,意味着i有父亲,但是一个有父亲的元素怎么能代表整个集合哪?至少得是它的父亲代表啊。
一层一层向上,我们便能找到一个祖先,它没有父亲。它便是那个长者,它可以代表整个集合的元素
得到结论:代表元素需满足fa[i] == i(初始化后没有父亲,但是有孩子…)

那好,一个元素中的孩子与另一个元素的孩子有关系,合并两个集合。我们只需要一层一层找父亲最终将一个的祖先指向另一个,大功告成!

再看上文的例子:我们找到2的祖先4,fa[4] = 6,再判断,2的祖先是…找到6,4的祖先…是6,那么它们有同一个祖先,看来是在一个集合里。

再仔细读一遍流程,我们会发现在查找和合并的时候我们找了i的两次祖先,但是明明在找祖先的过程中我们都遍历到了i的长辈们,还要做两次,感觉好像做了无用功

如果你并不这样认为,我看下面一个例子
10000000祖先是1,1000000的父亲是1000000-1,对于该集合的元素n,它的父亲是n-1(n != 1)。我想找到1000000的祖先,使它与a合并。那么我一层一层的爬…爬了1000000-1次,终于找到了1,于是我们将1000000的集合与a的集合愉快地合并了。

我现在突然傻了,记不住a和1000000的关系了。于是我又开始找父亲了…又是1000000-1次寻找,我终于发现它们俩是一个集合。1s中你能操作1000000-1次的寻找几次?哇TLE了!
由此引出了并查集的真正核心——路径压缩


我们最开始的寻找(find_fa)的代码应该是这样的

while(fa[i] != i)
{
    i = fa[i];
}

最终的i便是祖先
但是如果我们压缩一下路径,像这样:

int find_fa(int a){
    if(a != fa[a])  fa[a] = find_fa(fa[a]);
    return fa[a];
}

这段代码什么意思

效果是这样的:
原:n->n-1->n-2->,,,->2->1;
现:n->1, n-1->1, n-2->12->1;

我们递归寻找,找到后回溯更新所有遍历到的节点的父亲将它们指向祖先,一共是2*n次操作。如果不这么操作,改用朴素法,询问所有元素的祖先我们需要进行(1+2+…+n)次操作,即(1+n) * n/2次操作,而压缩完路径,我们只需要n次操作。两种查询的效率根本不在一个数量级上


那么开篇的那道题,是一个很好的板子题。
源代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 5005;
int n ,m, p;
int qi[maxn];
int rel(int a){
    if(a != qi[a])  qi[a] = rel(qi[a]);
    return qi[a];
}
int main(){
    //freopen("test.in", "r", stdin);
    scanf("%d%d%d", &n, &m, &p);
    for(int i = 1; i != n+1; ++i)
        qi[i] = i;//初始化
    for(int i = 0; i != m; ++i){
        int c1, c2;
        scanf("%d%d", &c1, &c2);        
        c1 = rel(c1);//找c1祖先
        c2 = rel(c2);//找c2祖先
        qi[c2] = c1;//c2祖先指向c1祖先
    }
    for(int i = 0; i != p; ++i){
        int c1, c2;
        scanf("%d%d", &c1, &c2);
        if(rel(c1) == rel(c2))//一个祖先
            cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    return 0;
}

由此并查集便完成了。
箜瑟_qi 2017.04.09 23:48

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值