引子:
现在来看这样一个经典问题:
亲戚
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定: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->1… 2->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