C语言——亲戚问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言——亲戚问题相关的知识,希望对你有一定的参考价值。

亲戚

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

数据输入:
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Ai和Bi具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

数据输出:
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
9
样例:
input.txt
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
output.txt
Yes
Yes
No
补充一下,文件名应是:relation

参考技术A 太晕了!! 参考技术B 出题者斩了!

并查集

在现实生活中,我们知道给出一些亲戚关系的信息,如A和B是亲戚,B和C也是亲戚,那么我们可以得出A和C也是亲戚。这是so easy 的的。我们看看下面的例子:

输入部分:给定N个人,M对数字,这些数字对表示某两个人是亲戚。接下来给定一个Q,表示Q对提问,求这些提问对中二者是否为亲戚

10 7 //N=10,M =7
2 4
5 7
1 3
8 9
1 2
5 6
2 3
3//Q
3 4
7 10
8 9
这类问题,数据量多的时候,我们可以采用集合的方式求解
关系             等价类/集合
10个人         1 2 3 4 5 6 7 8 9 10
(2,4) 是亲戚,合并集合    1 2,4 3 5 6 7 8 9 10
(5,7) ...         1 2,4 3 5,7 6 8 9 10
(1,3) ...              1,3 2,4 5,7 6 8 9 10
(8,9) ...         1,3 2,4 5,7 6 8,9 10
(1,2) ...         1,2,3,4 5,7 6 8,9 10
(5,6) ...         1,2,3,4 5,6,7 8,9 10
(2,3) ...         1,2,3,4 5,6,7 8,9 10
最后根据提问的要求,判断是否在一个集合就可以判断是否为亲戚了。
实现并查集的算法实现(O(log2n)):
技术图片
 1 struct node
 2 
 3     /* data */
 4     int date;//结点对应的下标
 5     int parent;//双亲结点
 6     int rank;//结点对应秩(并查集树的深度)
 7     int relation; //与父节点的关系
 8 ;
 9 
10 class DisJoinSet
11 
12 protected:
13     int n;//元素个数
14     node *tree;//并查集元素数组
15 public:
16     DisJoinSet(int n );
17     ~DisJoinSet();
18     void Init();
19     int Find(int x);// 查找x的代表元素(根),查找的同时进行路径压缩
20     void Union(int x,int y);
21     int GetAnswer();// 合并x和y
22 ;
View Code

一般我们用结构题存放每个元素,包括编号和rank,父亲结点,relation(与上层结点的关系(不少题目需要这样的设置))

技术图片
 1 DisJoinSet::DisJoinSet(int n)
 2 
 3     this->n = n ;
 4     tree = new node [n+1];
 5     Init();
 6 
 7 
 8 void DisJoinSet ::Init()
 9 
10     for(int i = 1 ;i <=n; i++)//顶点编号 0~n-1  或  1 ~ n都 行 
11     
12         tree[i].parent = i;//双亲初始化指向自已
13         tree[i].rank = 0;//秩初始化为0
14         tree[i].date = i;//编号
15         tree[i].relation = 0;    //i自己是一类,它的父节点此时就是它自己,属于同一类
16     
17 
18 
19 DisJoinSet::~DisJoinSet()
20 
21     delete []tree;
22 
View Code

初始化过程,首先每个元素都是单独一个集合,所以父亲结点指向自己本身。

技术图片
 1 int DisJoinSet::Find(int x)//查找x的代表元素(根),查找的同时进行路径压缩
 2 
 3     int temp = tree[x].parent;// 将x父节点的下标存入temp
 4     if( x != tree[x].parent)//若双亲不是自已
 5     
 6         tree[x].parent = Find(tree[x].parent);//递归在双亲中找x
 7         return tree[x].parent;// 返回根节点下标
 8     
 9     return x;//若双亲是自已,返回x
10 
View Code

这里我们提到一个路径压缩的问题。实际上是这样的。在查找到根结点的时候,我们把属于这个根所在集合的所有元素都直接挂在根的下边。即构成一个2层高,很宽的树型。像这样

技术图片

在对每一个元素进行查找所在集合(根)的时候,都递归的将其父亲结点改为根结点。这样做的目的是为了溯源方便。根就比如一个大学,那么多的大学生所对应的大学肯定不一样。如果第一种情况,别人问逆你的根(所在集合/大学)时是什么,那么你就得问你的父亲结点(辅导员),辅导员要问他的父亲结点(系主任),系主任要问他的父亲结点(院长),院长则要问校长,校长再一次次反馈下来你的学校是什么,就很浪费时间。而路径压缩过的好处就是,每个学生都直接挂在学校下面,对一次Find,都能很快反馈学校信息。这样是很方便的。

技术图片
 1 // 合并x和y
 2 void DisJoinSet::Union(int x, int y)
 3 
 4     int rootx = Find(x); // 找到下标为x的元素的根节点下标rootx
 5     int rooty = Find(y); // 找到下标为y的元素的根节点下标rooty
 6     if (rootx == rooty) // 已合并,还搞个毛,直接返回
 7     
 8         return;
 9     
10 
11     if (tree[rootx].rank > tree[rooty].rank)    //rooty结点的秩(深度)小于rootx结点的秩
12     
13         tree[rooty].parent = rootx;    //将rooty连到rootx结点上,rootx作为rooty的孩子结点
14     
15     else    //rooty结点的秩大于等于rootx结点的秩
16     
17         tree[rootx].parent = rooty;    //将rootx连到rooty结点上,rooty作为rootx的孩子结点
18         if (tree[rootx].rank == tree[rooty].rank)    //rootx和rooty结点的秩(深度)相同
19         
20             tree[rooty].rank++;        //rooty结点的秩增1
21         
22     
23 
View Code

这段合并操作,如何二者所在集合(rootx,rooty)相同,说明已经是一类了,直接返回。

否则,进行一个判断,判断两个即将合并的集合哪个的秩小些, 将小的那个集合的根挂到大的集合的根下边。稍微思考下,如果反过来,那么原来在秩大的那个集合的叶子Find是又读了一层递归,现任效率低了些。

这只是并查集的基本框架,对于不同题目的不同要求,还要灵活的处理才行。

以上是关于C语言——亲戚问题的主要内容,如果未能解决你的问题,请参考以下文章

处女作-----开始自己的技术分享之路

处女作-----开始自己的技术分享之路

C++ 学三天,能到什么程度?

并查集

并查集(初步)

C语言数组问题?