热门标签 | HotTags
当前位置:  开发笔记 > 人工智能 > 正文

腾讯二面算法题:朋友圈问题

大家好,我是程序员学长~今天我们来分享一道腾讯二面算法题,盆友圈问题~如果喜欢,记得点波关注吧~朋友圈问题现在有105个用户,编号为1-105。已知有m对关系,每一对关系给你两个数

大家好,我是程序员学长~

今天我们来分享一道腾讯二面算法题,盆友圈问题~

如果喜欢,记得点波关注吧~


朋友圈问题

现在有 105个用户,编号为 1- 105。已知有 m 对关系,每一对关系给你两个数 x 和 y ,代表编号为 x 的用户和编号为 y 的用户是在一个圈子中,例如: A 和 B 在一个圈子中, B 和 C 在一个圈子中,那么 A , B , C 就在一个圈子中。现在想知道最多的一个圈子内有多少个用户。

数据范围:1<= m <= 2 * 10 6

进阶:空间复杂度 O(n),时间复杂度 O(nlogn)。


输入描述:

第一行输入一个整数T,接下来有T组测试数据。对于每一组测试数据:第一行输入1个整数n,代表有n对关系。接下来n行,每一行输入两个数x和y,代表编号为x和编号为y的用户在同一个圈子里。

1 ≤ T ≤ 10

1 ≤ n ≤ 2 * 106

1 ≤ x, y ≤ 105


输出描述:

对于每组数据,输出一个答案代表一个圈子内的最多人数。

示例:

输入:

2
4
1 2
3 4
5 6
1 6
4
1 2
3 4
5 6
7 8

输出:

4
2

分析问题

通过分析题目,我们可以知道,这道题是求元素分组的问题,即将所有用户分配到不相交的圈子中,然后求出所有圈子中人数最多的那个圈子。

很显然,我们可以使用并查集来求解

首先,我们来看一下什么是并查集。

并查集是用来将一系列的元素分组到不相交的集合中,并支持合并和查询操作。



  • 合并(Union):把两个不相交的集合合并为一个集合。

  • 查询(Find):查询两个元素是否在同一个集合中。

并查集的重要思想在于,用集合中的一个元素代表集合

理论总是过于抽象化,下面我们通过一个例子来说明并查集是如何运作的。

我们这里把集合比喻成帮派,而集合中的代表就是帮主。

一开始,江湖纷争四起,所有大侠各自为战,他们每个人都是自己的帮主(对于只有一个元素的集合,代表元素自然就是唯一的那个元素)。

有一天,江湖人士张三和李四偶遇,都想把对方招募到麾下,于是他们进行了一场比武,结果张三赢了,于是把李四招募到了麾下,那么李四的帮主就变成了张三(合并两个集合,帮主就是这个集合的代表元素)。

然后,李四又和王五偶遇,两个人互相不服,于是他们进行了一场比武,结果李四又输了(李四怎么那么菜呢),此时李四能乖乖认怂,加入王五的帮派吗?那当然是不可能!! 此时的李四已经不再是一个人在战斗,于是他呼叫他的老大张三来,张三听说小弟被欺负了,那必须收拾他!!于是和王五比试了一番,结果张三赢了,然后把王五也拉入了麾下(其实李四没必要和王五比试,因为李四比较怂,直接找大哥来收拾王五即可)。此时王五的帮主也是张三了。

我们假设张三二,李四二也进行了帮派的合并,江湖局势变成了如下的样子,形成了两大帮派。

通过上图,我们可以知道,每个帮派(一个集合)是一个状的结构。

要想寻找到集合的代表元素(帮主),只需要一层层往上访问父节点,直达树的根节点即可。其中根节点的父节点是它自己。

采用这个方法,我们就可以写出最简单版本的并查集代码。



  1. 初始化

    我们用数组 fa 来存储每个元素的父节点(这里每个元素有且只有一个父节点)。一开始,他们各自为战,我们将它们的父节点设为自己(假设目前有编号为1~n的n个元素)。

    def __init__(self,n):
    self.fa=[0]*(n+1)
    for i in range(1,n+1):
    self.fa[i]=i


  2. 查询

    这里我们使用递归的方式查找某个元素的代表元素,即一层一层的访问父节点,直至根节点(根节点是指其父节点是其本身的节点)。

    def find(self,x):
    if self.fa[x]==x:
    return x
    else:
    return self.find(self.fa[x])


  3. 合并

    我们先找到两个元素的根节点,然后将前者的父节点设为后者即可。当然也可以将后者的父节点设为前者,这里暂时不重要。后面会给出一个更合理的比较方法。

    def merge(self,x,y):
    x_root=self.find(x)
    y_root=self.find(y)
    self.fa[x_root]=y_root


整体代码如下所示。

class Solution(object):
def __init__(self,n):
self.fa=[0]*(n+1)
for i in range(1,n+1):
self.fa[i]=i
def find(self,x):
if self.fa[x]==x:
return x
else:
return self.find(self.fa[x])
def merge(self,x,y):
x_root=self.find(x)
y_root=self.find(y)
self.fa[x_root]=y_root

优化

上述最简单的并查集代码的效率比较低。假设目前的集合情况如下所示。

此时要调用merge(2,4)函数,于是从2找到1,然后执行f[1]=4,即此时的集合情况变成如下形式。

然后我们执行merge(2,5)函数,于是从2找到1,然后找到4,最后执行f[4]=5,即此时的集合情况变成如下形式。

一直执行下去,我们就会发现该算法可能会形成一条长长的链,随着链越来越长,我们想要从底部找到根节点会变得越来越难。

所以就需要进行优化处理,这里我们可以使用路径压缩的方法,即使每个元素到根节点的路径尽可能的短。

具体来说,我们在查询的过程中,把沿途的每个节点的父节点都设置为根节点即可。那么下次再查询时,就可以很简单的获取到元素的根节点了。代码如下所示:

def find(self,x):
if x==self.fa[x]:
return x
else:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]

经过路径压缩后,并查集代码的时间复杂度已经很低了。

下面我们再来进一步的进行优化处理---按秩合并

这里我们需要先说明一点,因为路径压缩优化只是在查询时进行的,也只能压缩一条路径,因此经过路径优化后,并查集最终的结构仍然可能是比较复杂的。假设,我们现在有一颗比较复杂的树和一个元素进行合并操作。

如果此时我们要merge(1,6),我们应该把6的父节点设为1。因为如果把1的父节点设为6,会使树的深度加深,这样就会使树中的每个元素到根节点的距离都变长了,从而使得之后我们寻找根节点的路径也就会相应的变长。而如果把6的父节点设为1,就不会出现这个问题。

这就启发我们应该把简单的树往复杂的树上去合并,因为这样合并后,到根节点距离变长的节点个数比较少。

具体来说,我们用一个数组rank 来记录每个根节点对应的树的深度(如果对应元素不是树的根节点,其rank值相当于以它作为根节点的子树的深度)。

初始时,把所有元素的rank设为1。在合并时,比较两个根节点,把rank较小者往较大者上合并。

下面我们来看一下代码的实现。

def merge(self,x,y):
#找个两个元素对应的根节点
x_root=self.find(x)
y_root=self.find(y)

if self.rank[x_root] <= self.rank[y_root]:
self.fa[x_root]=y_root
else:
self.fa[y_root] = x_root

#如果深度相同且根节点不同,则新的根节点的深度
if self.rank[x_root] == self.rank[y_root] \
and x_root != y_root:
self.rank[y_root]=self.rank[y_root]+1

所以,我们终极版的并查集代码如下所示。

class Solution(object):
def __init__(self,n):
self.fa=[0]*(n+1)
self.rank=[0]*(n+1)
for i in range(1,n+1):
self.fa[i]=i
self.rank[i]=i
def find(self,x):
if x==self.fa[x]:
return x
else:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]
def merge(self,x,y):
#找个两个元素对应的根节点
x_root=self.find(x)
y_root=self.find(y)
if self.rank[x_root] <= self.rank[y_root]:
self.fa[x_root]=y_root
else:
self.fa[y_root] = x_root
#如果深度相同且根节点不同,则新的根节点的深度
if self.rank[x_root] == self.rank[y_root] \
and x_root != y_root:
self.rank[y_root]=self.rank[y_root]+1

有了并查集的思想,那我们这道朋友圈的问题就迎刃而解了。下面我们给出可以AC的代码。

class Solution(object):
def __init__(self,n):
self.fa=[0]*(n+1)
self.rank=[0]*(n+1)
self.node_num=[0]*(n+1)
for i in range(1,n+1):
self.fa[i]=i
self.rank[i]=1
self.node_num[i]=1
def find(self,x):
if x==self.fa[x]:
return x
else:
self.fa[x] = self.find(self.fa[x])
return self.fa[x]
def merge(self,x,y):
#找个两个元素对应的根节点
x_root=self.find(x)
y_root=self.find(y)
if self.rank[x_root] <= self.rank[y_root]:
#将x_root集合合并到y_root上
self.fa[x_root]=y_root
self.node_num[y_root] = self.node_num[y_root] + self.node_num[x_root]
else:
#将y_root集合合并到x_root上
self.fa[y_root] = x_root
self.node_num[x_root] = self.node_num[x_root] + self.node_num[y_root]
#如果深度相同且根节点不同,则新的根节点的深度
if self.rank[x_root] == self.rank[y_root] \
and x_root != y_root:
self.rank[y_root]=self.rank[y_root]+1
if __name__ == '__main__':
#最多有N个用户
N=100000
result=[]
T = int(input("请输入多少组检测数据?"))
while T>0:
n = int(input("输入多少对用户关系"))
print("输入{}组用户关系".format(n))
s1=Solution(N)
for i in range(n):
cur=input()
cur_users=cur.split(" ")
s1.merge(int(cur_users[0]), int(cur_users[1]))
max_people=1
for i in range(len(s1.node_num)):
max_people=max(max_people, s1.node_num[i])
result.append(max_people)
T=T-1
for x in result:
print(x)

到此,我们的并查集就聊完了。


啰嗦一句

现在给出一个思考题,可以把你的思考写在留言区。


现在给出某个亲戚关系图,判断任意给出的两个人是否具有亲戚关系。


原创不易!各位小伙伴觉得文章不错的话,不妨点赞(在看)、留言、转发三连走起!

你知道的越多,你的思维越开阔。我们下期再见。



推荐阅读
  • 深入理解C++中的KMP算法:高效字符串匹配的利器
    本文详细介绍C++中实现KMP算法的方法,探讨其在字符串匹配问题上的优势。通过对比暴力匹配(BF)算法,展示KMP算法如何利用前缀表优化匹配过程,显著提升效率。 ... [详细]
  • 自学编程与计算机专业背景者的差异分析
    本文探讨了自学编程者和计算机专业毕业生在技能、知识结构及职业发展上的不同之处,结合实际案例分析两者的优势与劣势。 ... [详细]
  • 线性Kalman滤波器在多自由度车辆悬架主动控制中的应用研究
    本文探讨了线性Kalman滤波器(LKF)在不同自由度(2、4、7)的车辆悬架系统中进行主动控制的应用。通过详细的仿真分析,展示了LKF在提升悬架性能方面的潜力,并总结了调参过程中的关键要点。 ... [详细]
  • 深入理解Java中的volatile、内存屏障与CPU指令
    本文详细探讨了Java中volatile关键字的作用机制,以及其与内存屏障和CPU指令之间的关系。通过具体示例和专业解析,帮助读者更好地理解多线程编程中的同步问题。 ... [详细]
  • 本章将深入探讨移动 UI 设计的核心原则,帮助开发者构建简洁、高效且用户友好的界面。通过学习设计规则和用户体验优化技巧,您将能够创建出既美观又实用的移动应用。 ... [详细]
  • 利用存储过程构建年度日历表的详细指南
    本文将介绍如何使用SQL存储过程创建一个完整的年度日历表。通过实例演示,帮助读者掌握存储过程的应用技巧,并提供详细的代码解析和执行步骤。 ... [详细]
  • SQLite 动态创建多个表的需求在网络上有不少讨论,但很少有详细的解决方案。本文将介绍如何在 Qt 环境中使用 QString 类轻松实现 SQLite 表的动态创建,并提供详细的步骤和示例代码。 ... [详细]
  • MySQL 数据库迁移指南:从本地到远程及磁盘间迁移
    本文详细介绍了如何在不同场景下进行 MySQL 数据库的迁移,包括从一个硬盘迁移到另一个硬盘、从一台计算机迁移到另一台计算机,以及解决迁移过程中可能遇到的问题。 ... [详细]
  • 阅读本文大约需要3分钟。微信8.0版本的发布带来了许多令人振奋的新功能,如烟花特效和改进的悬浮窗,引发了用户的热烈反响。 ... [详细]
  • 深入理解Java泛型:JDK 5的新特性
    本文详细介绍了Java泛型的概念及其在JDK 5中的应用,通过具体代码示例解释了泛型的引入、作用和优势。同时,探讨了泛型类、泛型方法和泛型接口的实现,并深入讲解了通配符的使用。 ... [详细]
  • 作为一名程序员,从大学步入职场后,常常感受到一种难以言喻的空虚感。这种感觉或许源于对生活的不满、职业发展的瓶颈,或是日常琐事带来的压力。本文将深入探讨这种复杂的情感,并尝试寻找解决之道。 ... [详细]
  • 深入解析:阿里实战 SpringCloud 微服务架构与应用
    本文将详细介绍 SpringCloud 在微服务架构中的应用,涵盖入门、实战和案例分析。通过丰富的代码示例和实际项目经验,帮助读者全面掌握 SpringCloud 的核心技术和最佳实践。 ... [详细]
  • 并发编程:深入理解设计原理与优化
    本文探讨了并发编程中的关键设计原则,特别是Java内存模型(JMM)的happens-before规则及其对多线程编程的影响。文章详细介绍了DCL双重检查锁定模式的问题及解决方案,并总结了不同处理器和内存模型之间的关系,旨在为程序员提供更深入的理解和最佳实践。 ... [详细]
  • 本文深入探讨了C++对象模型中的一些细节问题,特别是虚拟继承和析构函数的处理。通过具体代码示例和详细分析,揭示了书中某些观点的不足之处,并提供了更合理的解释。 ... [详细]
  • 随着网络安全威胁的不断演变,电子邮件系统成为攻击者频繁利用的目标。本文详细探讨了电子邮件系统中的常见漏洞及其潜在风险,并提供了专业的防护建议。 ... [详细]
author-avatar
莫名2602913353
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有