赞
踩
这个数据量应该是不能用暴力的办法求解,那我们应该怎么办呢?
由此,我们引出一种新的数据结构:并查集
有 n n n 个人一起吃饭,有些人互相认识。认识的人想坐在一起,不想跟陌生人坐。例如 A A A 认识 B B B, B B B 认识 C C C,那么 A A A、 B B B、 C C C会坐在一张桌子上。给出认识的人的关系,问需要多少张桌子。
我们可以根据上文的描述,得到如下并查集代码:
#include<bits/stdc++.h> using namespace std; const int N = 1007; int f[N]; //并查集 void init(){ //初始化 for(int i = 1; i <= N; i++){ f[i] = i; } } int find_father(int x){ //找自己的集 if(f[x] == x)return x; else return find_father(f[x]); } void union_set(int x, int y){ //合并 x = find_father(x); y = find_father(y); if(x != y)f[x] = f[y]; } int main(){ init(); int n, m, x, y; cin >> n >> m; for(int i = 1; i <= m; i++){ cin >> x >> y; union_set(x, y); } int cnt = 0; //记录集的数量 for(int i = 1; i <= n; i++){ if(f[i] == i){ cnt ++; } } cout << cnt; return 0; }
在上述程序中,查找、合并、的搜索深度是树的长度,复杂度都是 O ( n ) O_{(n)} O(n),性能比较差。下面介绍合并和查找的优化方法,优化之后,查找和合并的复杂度都小于 O ( l o g 2 n ) O(log_2n) O(log2n)。
在合并元素 x x x和 y y y时先搜到它们的根结点,然后再合并这两个根结点,即把一个根结点的集改成另一个根结点。这两个根结点的高度不同,如果把高度较小的集合并到较大的集上,能减少树的高度。下面是优化后的代码,在初始化时用 h e i g h t [ i ] height[i] height[i] 定义元素 i i i 的高度,在合并时一同更改。
int high[N]; void init(){ //初始化 for(int i = 1; i <= N; i++){ f[i] = i; high[i] = 0; } } void union_set(int x, int y){ //优化合并 x = find_father(x); y = find_father(y); if(high[x] == high[y]){ high[x]++; f[y] = x; } else{ if(high[x] < high[y]){ f[x] = y; } else{ f[y] = x; } } }
在上面的查询程序
f
i
n
d
f
a
t
h
e
r
(
)
find_father()
findfather() 中,查询元素
i
i
i 所属的集需要搜索路径找到根结点,返回
的结果是根结点。这条搜索路径可能很长。如果在返回的时候顺便把
i
i
i 所属的集改成根结
点,如图所示,那么下次再搜的时候就能在
O
(
1
)
O_{(1)}
O(1) 的时间内得到结果。
int find_father(int x){ //优化的查询
if(f[x] != x)f[x] = find_father(f[x]);
return f[x];
}
这个方法称为路径压缩,因为在递归过程中,整个搜索路径上的元素(从元素 i i i 到根结点的所有元素)所属的集都被改为根结点。路径压缩不仅优化了下次查询,而且优化了合并,因为在合并时也用到了查询。
上面的代码用递归实现,如果数据规模太大,担心爆栈,可以用下面的非递归代码:
int find_father(int x) {
int r = x;
while (f[r] != r)r = f[r]; //找到根的位置
int i = x, j;
while(i != r){
j = f[i];
f[i] = r; //把路径的根统一
i = j;
}
return r;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。