在以下文章中,我们介绍了并查集算法;
javascript 不相交集简介(并查集算法):https://blog.csdn.net/hefeng_aspnet/article/details/142524938
C# 不相交集简介(并查集算法):https://blog.csdn.net/hefeng_aspnet/article/details/142524824
python 不相交集简介(并查集算法):https://blog.csdn.net/hefeng_aspnet/article/details/142524698
java 不相交集简介(并查集算法):https://blog.csdn.net/hefeng_aspnet/article/details/142524698
c++ 不相交集简介(并查集算法):https://blog.csdn.net/hefeng_aspnet/article/details/142523025
并用它来检测图中的循环。我们对子集使用了以下union()和find()操作。
# Naive implementation of find
def find(parent, i):
if (parent[i] == -1):
return i
return find(parent, parent[i])
# Naive implementation of union()
def Union(parent, x, y):
xset = find(parent, x)
yset = find(parent, y)
parent[xset] = yset
# This code is contributed by rutvik_56
上面的union()和find()很简单,最坏情况下的时间复杂度是线性的。为表示子集而创建的树可能会歪斜,并且可能变得像链表一样。以下是一个最坏情况的示例。
假设有 4 个元素 0、1、2、3
最初,所有元素都是单元素子集。
0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
2 3
/
1
/
0
Do Union(2, 3)
3
/
2
/
1
/
0
图示:
在最坏情况下,上述操作可以优化为O(Log n) 。其思想是始终将深度较小的树附加在较深树的根下。此技术称为按等级联合。术语“等级”优于“高度”,因为如果使用路径压缩技术(我们在下面讨论过),则等级并不总是等于高度。此外,树的大小(代替高度)也可以用作等级。使用大小作为等级也会导致最坏情况的时间复杂度为 O(Logn)。
让我们看一下上面按等级合并的示例
最初,所有元素都是单元素子集。
0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
1 3
/ \
0 2
Do Union(2, 3)
1
/ | \
0 2 3
图示:
对简单方法的第二个优化是路径压缩。其思想是在调用find()时使树变平。当为元素 x 调用find()时,将返回树的根。find ()操作从 x 向上遍历以找到根。路径压缩的思想是将找到的根作为 x 的父节点,这样我们就不必再次遍历所有中间节点。如果 x 是子树的根,则 x 下所有节点的路径(到根)也会压缩。
假设子集 {0, 1, .. 9} 如下所示,并对
元素 3调用 find()。
9
/ | \
4 5 6
/ / \
0 7 8
/
3
/ \
1 2
图示:
当为 3 调用find()时,我们向上遍历并找到 9 作为
此子集的代表。通过路径压缩,我们还将 3 和 0 作为 9 的子元素,以便
下次为 0、1、2 或 3 调用 find() 时,到根的路径会减少。
--------9-------
/ / / \ \
0 4 5 6 3
/ \ / \
7 8 1 2
图示:
这两种技术 - 路径压缩和按等级/大小合并,时间复杂度将达到接近常数时间。事实证明,最终的摊销时间复杂度为 O(α(n)),其中 α(n) 是逆阿克曼函数,其增长非常稳定(当 n<10 600 时,它甚至不会超过)。
以下是按秩联合和基于路径压缩的实现来在图中查找循环:
# A union by rank and path compression based
# program to detect cycle in a graph
from collections import defaultdict
# a structure to represent a graph
class Graph:
def __init__(self, num_of_v):
self.num_of_v = num_of_v
self.edges = defaultdict(list)
# graph is represented as an
# array of edges
def add_edge(self, u, v):
self.edges[u].append(v)
class Subset:
def __init__(self, parent, rank):
self.parent = parent
self.rank = rank
# A utility function to find set of an element
# node(uses path compression technique)
def find(subsets, node):
if subsets[node].parent != node:
subsets[node].parent = find(subsets, subsets[node].parent)
return subsets[node].parent
# A function that does union of two sets
# of u and v(uses union by rank)
def union(subsets, u, v):
# Attach smaller rank tree under root
# of high rank tree(Union by Rank)
if subsets[u].rank > subsets[v].rank:
subsets[v].parent = u
elif subsets[v].rank > subsets[u].rank:
subsets[u].parent = v
# If ranks are same, then make one as
# root and increment its rank by one
else:
subsets[v].parent = u
subsets[u].rank += 1
# The main function to check whether a given
# graph contains cycle or not
def isCycle(graph):
# Allocate memory for creating sets
subsets = []
for u in range(graph.num_of_v):
subsets.append(Subset(u, 0))
# Iterate through all edges of graph,
# find sets of both vertices of every
# edge, if sets are same, then there
# is cycle in graph.
for u in graph.edges:
u_rep = find(subsets, u)
for v in graph.edges[u]:
v_rep = find(subsets, v)
if u_rep == v_rep:
return True
else:
union(subsets, u_rep, v_rep)
# Driver Code
g = Graph(3)
# add edge 0-1
g.add_edge(0, 1)
# add edge 1-2
g.add_edge(1, 2)
# add edge 0-2
g.add_edge(0, 2)
if isCycle(g):
print('Graph contains cycle')
else:
print('Graph does not contain cycle')
# This code is contributed by
# Sampath Kumar Surine
输出
Graph contains cycle
时间复杂度: O(ElogV),其中E是图中的边数,V是顶点数。
空间复杂度: O(V),其中 V 是顶点的数量。这是因为我们使用子集数组来存储每个顶点的代表元素,并且该数组的大小与顶点数量成正比。