文章目录
引言
领导(笑呵呵的走过来):有个挑战性但很有意思的项目,你想不想试一下
小溪子:(各个部位有点紧)好的,非常想干。
领导:现在一个手机通常有几个名字,我们已经知道一些名字对代表相同手机,需要设计一个算法能够将一个手机的所有名字连接起来形成一个组,给定任一名字能够查找所属的组,并判断两个名字是否等效(相同手机)。
小溪子:(恰巧看算法)这好像是动态连通性问题。
动态连通性
有以下手机名称,已知(0,1) (0,2), (0,3), (3,4), (5,6), (5,7)代表相同手机。
NO | Names |
---|---|
0 | iPhone 8 |
1 | Apple A1863 |
2 | Apple A1906 |
3 | Apple A1905 |
4 | Apple A1907 |
5 | Oneplus A6003 |
6 | Oneplus A6000 |
7 | Oneplus 6 |
为了判断任意两个名称是否指相同手机,将所有手机名称都想象为一个点,每输入一个已知的名称对,将两个点连接在一起,依次输入所有已知名称对,就会形成多个连通图,用来判断任意两个名称是否指相同手机(两点是否连通),绘制为连通图如下:
动态连通性还可以应用到网络连接判断,位置间是否有道路相通等。
Union-Find
并差集是一种可以记录一系列节点分别属于哪个子集的数据结构,为了解决动态连通性问题,需要能够实现以下功能:
- union: 连通输入的一个节点对
- find: 给定一个节点查找属于哪个连通组
- is_connected: 判断两个节点是否连通。
实现算法及复杂度比较
Quick-find
利用键值对key-value保存每个节点的组号
No | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
Group | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
查找 find(5), find(8)时候,时间复杂度为1。
如果需要union(5, 8),则需要更改每个键值对,所以Union时间复杂度为N
所以键值对只实现了快速查找但合并复杂度高,如果对于大量数据,这是不可以接受的。
Quick-union
为了让Union的效率更高,不是逐一去更改每个节点的组号,需要一个新的数据结构来保存组信息,当合并时候只需更改一个节点,显然树结构可以满足此需要,用根节点保存组号,只要根节点改变,整棵树就修改了。但这样的话,Union提升了但查询效率又下降为树的高度,最坏时候多叉树会变为链表。
Weighted quick-union
为了减少树的高度,在Quick-union算法基础上,当Union时候同时记录每颗树的大小,总是把小树连接到大树,这样能保持树的平衡,树的高度会降低,所以find的效率会提高。
Weighted quick-union with path compression
在查询时候每次都是从根节点逐一回溯到根节点,如果将所有节点都变成根节点的子节点,树会扁平查询效率高,但这样会增加空间复杂度(反复开辟销毁内存),所以如果只是将节点变成他爷爷的孩子,相当于压缩了查询路径,提高了查询效率。
算法复杂度
算法 | union | find |
---|---|---|
quick-find | N | 1 |
quick-union | tree height | tree height |
weighted quick-union | lgN | lgN |
weighted quick-union with path compression | very near to 1 | very near to 1 |
Python 完整实现Weighted quick-union with path compression
class UnionFind(object):
def __init__(self, n_items, compress_path):
""" Make empty UF with n_items sites, each site initially alone in its own group """
self.count = n_items # capacity, current number of groups
self.compress_path = compress_path # shrink tree (speed-up)
self.parent = list(range(n_items)) # the tree
self.size = [1] * n_items # record the size of each subtree/group
def find(self, p):
while p != self.parent[p]:
if self.compress_path:
self.parent[p] = self.parent[self.parent[p]]
p = self.parent[p]
return p
def union(self, p, q):
if p == q:
return
rootp = self.find(p)
rootq = self.find(q)
if rootp == rootq:
return
# tree balancing to keep maximum depth smaller
if self.size[rootp] < self.size[rootq]:
self.parent[rootp] = rootq
self.size[rootq] += self.size[rootp]
else:
self.parent[rootq] = rootp
self.size[rootp] += self.size[rootq]
self.count -= 1
def is_connected(self, p, q):
return p == q or self.find(p) == self.find(q)
def merge_path(self, p):
steps = [p]
while p != self.parent[p]:
p = self.parent[p]
steps.append(p)
return steps
代码下载地址:https://gist.github.com/xidongsheng/d69a3af4291f8705a52df2820558d29f
参考文献
[1]https://algs4.cs.princeton.edu/15uf/
[2]https://en.wikipedia.org/wiki/Disjoint-set_data_structure