最近在赛码网上做算法题,看到这样一道题,经过不断的学习,最后解决了。把我的思想和代码给大家分享一下~
认老乡
题目描述
大学的同学来自全国各地,对于远离家乡步入陌生大学校园的大一新生来说,碰到老乡是多么激动的一件事,
于是大家都热衷于问身边的同学是否与自己同乡,来自新疆的小赛尤其热衷。但是大家都不告诉小赛他们来自哪里,
只是说与谁同乡,从所给的信息中,你能告诉小赛有多少人确定是她的同乡吗?
输入
每个测试实例首先包括2个整数,N(1 <= N <= 1000),M(0 <= M <= N*(N-1)/2),代表现有N个人(用1~N编号)和M组关系;
在接下来的M行里,每行包括2个整数,a,b,代表a跟b是同乡;
当N = 0,M = 0输入结束;
已知1表示小赛本人。
样例输入
3 1
2 3
5 4
1 2
3 4
2 5
3 2
0 0
输出
对于每个测试实例,输出一个整数,代表确定是小赛同乡的人数。
样例输出
0
4
时间限制
C/C++语言:1000MS其它语言:3000MS
内存限制
C/C++语言:65536KB其它语言:589824KB
作为一个没有搞过算法竞赛的普通大学生,当我拿到这道题的时候,自然的向数据机构去靠拢。
其实这就是一道图论问题。我们把每个人当成一个点,如果两个人是同乡,这两个点之间有一条通路。
根据输入用例,把所有同乡关系输入之后,也就能确定一个图当中有多少个点,哪些点之间是有连线(通路)的。
最后我们要统计小赛同学的同乡,换句话说,我们要看与1号点直接和间接有路、能到达的点的个数、也就是小赛同乡的个数。
分析完理论,说点实际的:
最开始拿到这样一道题,想到是图论,我想到的办法就是数据结构当中的邻接表去描述这个图论。
邻接表是什么样的呢?我带没有数据结构基础的朋友去理解一下:
有一个点表,可以用数组实现。比如这道题,我们可以开一个大数组,0号不存东西,下标就代表同学的编号。数组每个值存一个链表,链表的值是跟这个点直接相连关系的点的下标。
在这道题中,比如,1号元素代表1号学生,1号数组值存一个集合,集合内的元素是跟1号元素有通路的点的编号。
按照输入,向定点表的集合里添加数字。输入结束后,图就构建起来了。每一个元素代表一个点。元素存的集合里面元素就是跟它有通路的其他点。
最后我们查询跟1号点直接或和间接相连的点的个数,也就是它同乡的个数。
方法是:遍历每一个元素,如果这个元素在1号元素的集合里,或者这个元素存的集合里的值有在1号元素集合里的,或者有1号元素的,那都是1号元素的老乡。最后统计一个total就可以了。
按照上面的思想 我分别用c和python实现了:
邻接表的思想显而易见,我用python来实现:
1 coding:utf8 2 mn= raw_input().strip(" ").split(" ") 3 #接下来m个人 n组关系 4 m = int (mn[0] ) 5 n = int (mn[1] ) 6 7 # 如果m n 都是0 退出程序 8 if (m,n) == (0,0) : 9 exit() 10 11 #初始化一个 编号1到m的邻接表 0号不存数据 12 # 1到m号人每个人拥有一个集合 集合里放着这个人老乡的编号 接下来输入数据的时候向集合里面添加老乡 13 people = [ set() for i in range(m+1) ] 14 #循环输入n个关系 15 for i in range(n): 16 #输入的关系放入rel列表当中 17 rel = [ int(i) for i in raw_input().strip(" ").split(" ") ] 18 #给邻接表当中 每一个人添加老乡编号 rel[0] 和 rel[1]是老乡 把rel[1] 的数值 放进rel[0]的集合当中 19 people[rel[0]].add(rel[1]) 20 21 #遍历邻接表把1号的直接 间接的老乡 全都添加到1号的集合当中 22 for i in range(2,m+1): 23 # i 代表人的编号从2到m(不查看1号自己) 24 # 如果 编号i在 people[1]中 说明i是1的同乡, 把i的同乡全都加入到people[1]里 25 # 如果i的同乡有在 1 的同乡集合里的 说明他们是间接同乡 也把i号的同乡加入people[1] 26 if i in people[1] or (people[i] & people[1] ): 27 people[1] |= people[i] 28 people[1].add(i) 29 #最后 1号同学自己的集合里面元素的个数就是他老乡的个数 30 print(len(people[1]))
但是,当我提交代码的时候,发现邻接表的方法,是超出时间限制的。
至就是一个老老实实学习的大学生的弊端,跟搞过算法的同学相比确实有需要学习的东西。
经过不断查询,原来这种图论,某个点的连通问题可以用"并查集"的方法来解决。
并查集也是解决图论思想的一种时间复杂度很低的算法,思想是:
根据输入,确定点和点之间的连通。当点之间连通的时候,我们就把他们放到同一个集合当中。最后1所在集合的其他元素的个数就是1号元素老乡的数量。
具体怎么实现呢:
对于有n个编号点的图,我们开一个编号从0到n的数组。编号0我们不存数据。
我们把1到n号的值都初始化成自己的编号,即下表为n的值里面存n。代表每个点跟自己是连通的。
接下来根据输入,我们能确定某两个点的是连通的,这个时候,我们选定其中一个做根,另一个做叶子。叶子里面存根的编号。叶子和根一定是相连通的。根存自己的编号。
随着输入关系的增多,我们尽量让叶子和跟的层数保持最低,对于多个点有连关系,我们选定其中一个做根,根的数值是自己的编号,其他所有叶子都存这一个根的编号。这样我们就能确定根和所有叶子都是连通的。
按照上面的思想,再用python去实现一下,这个是并查集解决图论问题的思想:
1 #coding:utf8 2 3 def find(x): 4 global relation 5 if relation[x]!= x: 6 relation[x] = find(relation[x]) 7 return relation[x] 8 9 while True: 10 mn= input().strip(" ").split(" ") 11 #接下来m个人 n组关系 12 m = int (mn[0] ) 13 n = int (mn[1] ) 14 # 如果m n 都是0 退出程序 15 if (m,n) == (0,0) : 16 exit() 17 18 #初始化一个列表 ,编号i 里面存的与i有亲戚关系的同学,初始化的时候存着自己, 0号没有意义 19 relation = [ i for i in range( m+1 ) ] 20 21 #循环输入n个关系 22 for i in range(n): 23 #输入的关系放入rel列表当中 24 rel = [ int(i) for i in input().strip(" ").split(" ") ] 25 a = rel[0] 26 b = rel[1] 27 a = find(a) 28 b = find(b) 29 relation[a] = b 30 31 sum = 0 32 root = find(1) 33 for i in range(2,m+1): 34 if find(i) == root: 35 sum +=1 36 37 print(sum)
这段代码交上去,发现还是超时,无奈,最后用c++语言去实现了一下,总算是通过了。
c++语言代码如下:
1 #include "iostream" 2 using namespace std; 3 int person[1001]; 4 5 //查找并维护连通图的方法 6 int findroot( int a ){ 7 //要使能够连通的所有节点选一个做根,让所有其他节点指向这个跟,深度减小 方便遍历 8 if( person[a] != a ){ 9 //让叶子节点全都存一个根节点的编号 10 person[a] = findroot( person[a] ); 11 } 12 return person[a]; 13 14 } 15 16 17 int main(){ 18 int m, n; 19 while(cin>>m>>n){ 20 if( m == 0 && n == 0 ){ 21 break; 22 } 23 //初始化一个集合,每个元素的可连通对象都是自己 24 for( int i = 1; i <= m; i++){ 25 person[i] = i; 26 } 27 28 for( int i = 0; i < n ;i++ ){ 29 int a,b; 30 cin>>a>>b; 31 //输入了一组关系,我们调用方法,找到根节点,把他们都赋值根节点的编号 32 a = findroot(a); 33 b = findroot(b); 34 35 person[a] = b; 36 } 37 int sum = 0; 38 int root = findroot(1) ; 39 for(int i = 2 ; i <= m ;i++ ){ 40 //最后统计一下与1号节点根节点相同的有哪些点。也就是同乡的数量。 41 if(findroot(i)==root){ 42 sum ++; 43 } 44 } 45 cout<<sum<<endl; 46 } 47 return 0; 48 }