union-find算法是一个解决动态连通性的经典算法, 具有广泛性的应用. 简单起见, 我们将对象称为触点, 将整数对称为连接, 将等价类称为连通分量, 并用0 到N-1的整数来表示N个触点.
API
| |
成本模型: 以数组的访问次数作为成本模型, 无论读写.
算法的关键在于find和union的设计上.
quick-find算法
| |
quick-find使用一个触点为索引的数组作为数据结构, 每次find()调用访问数组一次, 而union()调用访问数组(N+3)到(2N+1)次. 假设我们最后只得到
了一个连通分量, 那么至少需要调用N-1次union(), 即至少$ (N+3)(N-1) \sim N^2 $次数组访问. 意味着该算法是平方级别的, 也意味着该算法不试用于
规模更大(百万级)的场景.
quick-union算法
这个算法在于提高union()的速度, 和上面的quick-find相互补.
| |
quick-union也使用一个触点为索引的数组作为数据结构, 但对应的元素都是同一个连通分量中另一个触点的名字(链接). 该算法将union()操作降低到线性级别,
但find()操作在最好的情况下只需访问一次资源, 而最坏的情况是2N+1(这是较为保守的估计, 因为while循环在编译后对id[p]的第二次引用一般不会访问数
组). 对于一般的输入数据而言, 是对quick-find算法较好的改进, 但对于有些输入并不会比quick-find快.
同样, 假设我们最后只得到了一个连通分量, 在最坏的情况下是平方级别的: 输入的整数都是有序数对, 0-1, 0-2, 0-3等, 此时find访问数组的次数为 $ 3+5+7+\cdots+(2N-1)\sim N^2 $
加权quick-union算法(union-find)
为了减少quick-union算法最坏情况出现, 将原来union()操作中随意将一棵树连接到另一棵树改为总是将较小的树连接到较大的树上.
| |
改进的目的在于降低树的高度, 以减少find()调用的访问数组次数. 在最坏的情况下, 成本模型的增长数量级为logN. 即加权quick-union算法在处理N个触点
和M条连接时最多访问数组cMlgN.
最优算法
理想情况下的最优算法应该能够保证在常数时间内完成union()和find()调用, 但这是非常困难的. 通常情况下, 路径压缩的加权算法quick-union算法是
最优算法:
| |
路径压缩算法将路径上遇到的所有都链接到根节点, 其和quick-find在理想情况下得到的树非常接近, 但并非所有操作都在常数时间完成. 但实际情况下已经不太可能 对加权quick-union算法进行任何改进了(笔者在测试数据为200W条时, 所得到的提升较小), 而且不存在其他算法能够保证union-find算法的所有操作在均摊后都 是常数级别的(在非常一般的cell probe模型之下, 即只记录对随机内存的访问, 内存大小足以保存所有输入且假设其他操作均没有成本). 使用路径压缩的加权quic k-union算法已经是我们对于这个问题能够给出的最优解.