problem
There are N students in a class. Some of them are friends, while some
are not. Their friendship is transitive in nature. For example, if A
is a direct friend of B, and B is a direct friend of C, then A is an
indirect friend of C. And we defined a friend circle is a group of
students who are direct or indirect friends.Given a N*N matrix M representing the friend relationship between
students in the class. If M[i][j] = 1, then the ith and jth students
are direct friends with each other, otherwise not. And you have to
output the total number of friend circles among all the students.
注意:间接朋友的间接朋友也算一个朋友圈,如a和b是间接朋友,b和c是间接朋友,那么a和c也是间接朋友。
分析
这个问题很像leetcode 565,都是把原集合分成若干个不相交的子集,子集的元素之间有着内在的联系,我们要做的就是通过这些联系把这些元素聚合起来,不同的是在leetcode 565中元素的联系是线性的,而在本问题中元素的联系可以抽象为一个图,所以在这里就可以使用图的深度优先搜索或宽度优先搜索来实现,其他的优化方法都和之前的思路差不多,例如使用visit list而不是set,使用 O(1) 的空间复杂度等,这里不再赘述。
class Solution(object):
def findCircleNum(self, M):
"""
:type M: List[List[int]]
:rtype: int
"""
n = len(M)
s = set()
ans = 0
def dfs(x):
for i, num in enumerate(M[x]):
if num == 1 and i not in s:
s.add(i)
dfs(i)
for i in range(n):
if i not in s:
dfs(i)
ans += 1
return ans
并查集
在阅读discussion的时候发现还可以使用并查集(union-find sets)来解决这个问题。
定义:
并查集可以实现两种操作:
find:确定元素属于哪个子集,可以用来判定两个元素是否来自同一个子集
union:将两个集合合并为一个集合
实现:
以上是对并查集抽象功能定义(也就是一个抽象数据类型ADT),在通常的实现上使用并查集森林来实现,即一个子集为一棵树,find(x)返回x所在树的根,如果两个元素的根相同则两个元素属于一个子集,union则是将两棵树合并为一棵树。
优化:
如果不优化的话树可能严重不平衡,导致查询效率不会比链表好。可以使用以下两种方法优化:
按秩合并:
即总是将更小的树连接到更大的树上,这样可以保证树的深度不会增加,除非他们两个的深度相同。
路径压缩:
在查找时将经过的的节点都直接连接在根上,通常使用递归方法:
function Find(x)
if x.parent != x
x.parent := Find(x.parent)
return x.parent
由于所有的find返回值都是root,所以在查找时可以把路径上的节点都直接连在根上,从而降低树的深度。
class UnionFind(object):
def __init__(self, n):
self.count = n
self.parent = [i for i in range(n)]
self.rank = [0] * n
def find(self, p):
if self.parent[p] != p:
self.parent[p] = self.find(self.parent[p])
return self.parent[p]
def union(self, p, q):
root_p = self.find(p)
root_q = self.find(q)
if root_p == root_q: return
if self.rank[root_p] < self.rank[root_q]:
self.parent[root_p] = root_q
else:
self.parent[root_q] = root_p
if self.rank[root_p] == self.rank[root_q]:
self.rank[root_q] += 1
self.count -= 1
介绍了并查集数据结构后自然可以想到解决这个问题的方法,即先初始化,然后对所有两个相连的子集都合并,最后find所有的节点看一共有多少不同的值,或在初始化和合并时就进行记录,即为朋友圈数。
总结
在这个问题中主要复习了一下图的dfs和bfs,对应树的dfs和bfs,由于树和图都不是线性的数据结构,所以不可能使用一个for循环就完成遍历,而是都需要不用不同的方式存储未遍历的节点,例如在dfs中通常使用递归的方式存储未遍历节点,例如
for i, num in enumerate(M[x]):
if num == 1 and i not in s:
s.add(i)
dfs(i)
可以看到,处理完当前节点就处理这个节点的子节点(对应树的先序遍历),递归时for循环记录当前处理进程,类似于操作系统中的断点,所以虽然没有显式的存储,但实际中还是进行了记录。
而在bfs中则通常使用队列来记录处理过程,对应树的层次遍历。
另外本题中使用的set来记录处理过的节点,通常在图或树的节点都有记录遍历的数据结构。