首页 > 代码库 > 赛码网算法:认老乡

赛码网算法:认老乡

最近在赛码网上做算法题,看到这样一道题,经过不断的学习,最后解决了。把我的思想和代码给大家分享一下~

 

认老乡
题目描述
大学的同学来自全国各地,对于远离家乡步入陌生大学校园的大一新生来说,碰到老乡是多么激动的一件事,
于是大家都热衷于问身边的同学是否与自己同乡,来自新疆的小赛尤其热衷。但是大家都不告诉小赛他们来自哪里,
只是说与谁同乡,从所给的信息中,你能告诉小赛有多少人确定是她的同乡吗?

输入
每个测试实例首先包括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 }

 

赛码网算法:认老乡