本文链接:http://i.cnblogs.com/EditPosts.aspx?postid=5408816
问题:
在某个城市里,住着N个人,这N个人都有自己的公司,现给出N个人的M条信息(即某两个人属于同一个公司),问这个城市最多有多少个公司。
THINK:
比如给出
10 6
1 3
3 7
2 5
5 9
9 10
6 8
代表有10个人,6个关系,1 和 3 在一个公司里, 3 和 7 在一个公司里,2 和 5 在一个公司里, 5 和 9 在一个公司里,9 和 10 在一个公司里, 6 和 8 在一个公司里。那么1 3 7则在一个公司里,2 5 9 10在另外一个公司里,4 单独在一个公司里,6 和8 在一个公司里,则这个城市最多有 4 个公司。
把这个问题数学化,即有N个元素,这N个元素都属于某一个集合,给出M个关系,代表某两个元素在一个集合里,问最多有多少个集合,今天讨论的并查集算法可以很好的解决这个问题。
并查集(Disjoint Set):
把这N个元素初始化为N个不相交集合,在每个集合里面选择其中某个元素代表所在集合的名字。
常见操作:
1):合并两个集合;
2):查找某元素所在集合
即“并查集”。
具体实现:
用编号最小的元素的标记某个集合。
定义一个数组pre[1...N],其中pre[i]代表i所在集合。
则最后的集合为:{1, 3, 7},{2, 5, 9, 10},{4}, {6, 8},第一个集合代号为 1 ,第二个集合代号为 2 ,第三个集合代号为 4 ,第四个集合代号为 6。
初始化代码:
void initPre() { for(int i = 1; i <= N; ++i) pre[i] = i; }
查找代码:
int Find(int x) { int r = x; while (pre[r] != r) r = pre[r]; return r; }
合并代码:
void mix(int x, int y) { int fx = Find(x); int fy = Find(y); if(fx < fy) pre[fy] = fx; if(fx > fy) pre[fx] = fy; }
初始化代码比较容易理解,下面来解释查找代码。拿上面第二个集合来说吧,比如查找 10 在哪个集合里面,前面说过,pre[i] 代表 i 所在集合的编号,那么看pre[10],因为pre[10] 等于 9 那么 10 在 代号为 “9” 的集合里面吗?这个显然不是,因为上面根本不存在代号为 “9” 的集合,这是为什么?之前说过用这个集合里面元素最小的数字代表这个集合的编号,则代表这个集合编号的元素的 pre[i] 一定等于 i,由于pre[9] != 9,所以继续往上找,找到了5,pre[5] != 5,那么继续向上找到 1 ,pre[1] = 1,即找到了代表 10 所在集合的编号为 “1”,这就是查找操作。
再来解释合并操作,比如我们要合并 8 和 10,我们先找到 8 所在集合的编号为 “6”,10 所造集合的编号为 “1”,由于 10 所在集合的编号小于 8 所在集合的编号,于是就可以让 8 所在集合的编号 “6” 改成 10 所在集合的编号 “1”,即 pre[6] = 1,于是就完成了合并操作。
到了这里你有没有觉得有什么不妥的地方呢?比如再次查找 10 所在的集合,是不是又得一步一步的向上查找?的确是的,可是刚才已经查找到了 10 所在的集合编号为 “1”,为什么还有查找呢?由于并没有改变 pre[10] 的值,那么每次查找都需要做这样的动作。所以当查找到10所在的集合编号为 “1” 时可以直接让pre[10] = 1。那么就这么结束了吗,还没有,由于在查找 10 的过程中还查找了 9 ,那么顺便还可以让pre[9] = 1,于是下次查询时就很省时间了,这就是所谓的路径压缩。
(推荐一个详解路径压缩的博客:http://blog.csdn.net/niushuai666/article/details/6662911)
含路径压缩的查找代码:
int Find(int x) { int r = x; while(r != pre[r]) r = pre[r]; int i = x, j; while(pre[i]!=r) { j = pre[i]; pre[i] = r; i = j; } return r; }
递归压行式:
int Find(int x){ return x == pre[x] ? x : pre[x] = Find(pre[x]); }