这个题,可以有三种解法。BFS,DFS,Union-Find。
BFS,DFS,可以理解为朋友圈是多个图/树,然后看有多少个独立的子图/树。
本文讲Union-Find解法,及其优化。下文讲BFS,DFS。
并查集中,有一个点要注意
。
如果两个node,有connection,但是father node不同,这时候要把father node进行连接。
注意,两个father node的的father node,其实是他自己。
所以,要将一个father node的father,连到另一个father node上!
这里第一次写出了错,还是并查集没有理解到位,要注意!
好了,这篇博客是希望你有一定的Union-Find的基础。
题目:
547. 朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出: 2
说明:已知学生0和学生1互为朋友,他们在一个朋友圈。
第2个学生自己在一个朋友圈。所以返回2。
示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出: 1
说明:已知学生0和学生1互为朋友,学生1和学生2互为朋友,所以学生0和学生2也是朋友,所以他们三个在一个朋友圈,返回1。
注意:
N 在[1,200]的范围内。
对于所有学生,有M[i][i] = 1。
如果有M[i][j] = 1,则有M[j][i] = 1。
我发现,再多的解释也不如code有力。自己有时候刷题看解释总是看不懂,仔细研读code,然后一下就明白了,code最有逻辑,不会出错。所以,上code:
class Solution:
def find(self, node):
while node != self.father[node]:
node = self.father[node]
return node
def connect(self, p1, p2):
father1 = self.find(p1)
father2 = self.find(p2)
if father1 != father2:
self.father[father1] = father2
self.cycle -= 1
def findCircleNum(self, M: List[List[int]]) -> int:
n = len(M)
self.cycle = n
self.father = list(range(n))
for i in range(n):
for j in range(i+1, n):
if M[i][j]==1:
self.connect(i,j)
return self.cycle
# Your runtime beats 17.8 % of python3 submissions
# Your memory usage beats 11.11 % of python3 submissions (13.9 MB)
总的来说,其实就是维护了每个node的father,father的father,。。。不断的father(即:代码里的self.father)
其实还有优化的空间:
- 平衡性优化:小的树接到大的树下面,体现在代码里是connect的时候,要判断数的大小。所以采用一个额外的数组记录数的大小。
- 路径压缩:对于一个子树来说,不以为他是一长串,每次找father的时候,都要一个一个往上,复杂度是O(n)。我们希望他是并排铺开的,这样找father就是O(1)。体现在代码里是find的时候,做一个路径压缩。实现压缩每棵树的高度。
这里说的不清楚的话,可以看下这个题解。
好了,先做优化1。实现小树接大树。
code:
class Solution:
def find(self, node):
while node != self.father[node]:
node = self.father[node]
return node
def connect(self, p1, p2):
father1 = self.find(p1)
father2 = self.find(p2)
if father1 != father2:
if self.size[father1] > self.size[father2]: # father1树比father2数的大,所以2接1.
self.father[father2] = father1 # 把2的father变成1的father
self.size[father1] += self.size[father2] # father1膨胀了
else: # 情况相反
self.father[father1] = father2
self.size[father2] += self.size[father1]
self.cycle -= 1
def findCircleNum(self, M: List[List[int]]) -> int:
n = len(M)
self.cycle = n
self.father = list(range(n))
self.size = [1 for _ in range(n)]
for i in range(n):
for j in range(i+1, n):
if M[i][j]==1:
self.connect(i,j)
return self.cycle
# Your runtime beats 82.96 % of python3 submissions
# Your memory usage beats 11.11 % of python3 submissions
然后在优化1的基础上,做优化2。压缩每棵树的高度。实现找father是复杂度的降低。
code:
class Solution:
def find(self, node):
while node != self.father[node]:
# 找他爸太花时间,因为他爸还要找他爸,所以直接找他爷爷。
# 把他爷爷当作他爸,来缩小树的高度。
self.father[node] = self.father[self.father[node]]
# 接着while
node = self.father[node]
return node
def connect(self, p1, p2):
father1 = self.find(p1)
father2 = self.find(p2)
if father1 != father2:
if self.size[father1] > self.size[father2]: # father1树比father2数的大,所以2接1.
self.father[father2] = father1 # 把2的father变成1的father
self.size[father1] += self.size[father2] # father1膨胀了
else: # 情况相反
self.father[father1] = father2
self.size[father2] += self.size[father1]
self.cycle -= 1
def findCircleNum(self, M: List[List[int]]) -> int:
n = len(M)
self.cycle = n
self.father = list(range(n))
self.size = [1 for _ in range(n)]
for i in range(n):
for j in range(i+1, n):
if M[i][j]==1:
self.connect(i,j)
return self.cycle
# Your runtime beats 82.96 % of python3 submissions
# Your memory usage beats 11.11 % of python3 submissions
不过,这个提交的时间,说明不了什么。这个和服务器当前的运行状态关系比较大。没啥用。。。
我们应该从算法本身去降低时间/空间复杂度。这应该是优化的初衷。
好了,今天就到这里了。