leetcode(547). Friend Circles

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来记录处理过的节点,通常在图或树的节点都有记录遍历的数据结构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值