亲属关系--并查集训练T1
来源:JK老班
问题描述:若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
规定: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个询问的答案为“具有”或“不具有”亲戚关系。
输入样例:
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
输出样例:
Yes
Yes
No
方法一:
a与b,找a可到的所有点。(深度优先DFS或广度优先BFS)。
2和3,若3在2的连通分支,是亲戚关系。
DFS(a),连通分支若包含B,Yes,若不含b,No。
import java.util.ArrayList;
import java.util.Scanner;public class p1569 {
ArrayList[] AL;
int n,m,T;
boolean Judge(int x,int y) {
ArrayList<Integer> Q=new ArrayList<Integer>();
boolean[] Mark=new boolean[n+1];
Q.add(x); Mark[x]=true;
int front=0;//队首指针
while(front<Q.size()) {//队列非空
int head=Q.get(front);front++;
if(head==y) return true;
for(int i=0;i<AL[head].size();i++) {
int adj=(int) AL[head].get(i);
if(Mark[adj]==false) {
Q.add(adj);Mark[adj]=true;
}
}
}//while
return false;//队列都空了仍没有
}
public p1569() {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();m=sc.nextInt();T=sc.nextInt();
AL=new ArrayList[n+1];
for(int i=1;i<=n;i++) AL[i]=new ArrayList<Integer>();
for(int i=0;i<m;i++) {
int x=sc.nextInt(),y=sc.nextInt();
AL[x].add(y);AL[y].add(x);//邻接点加入
}
for(int i=0;i<T;i++) {
int x=sc.nextInt(),y=sc.nextInt();
System.out.println(Judge(x,y) ? "Yes":"No");
}
}
public static void main(String[] args) {
p1569 p=new p1569();
}}
方法二:
集合没有很好的实现,比如:合并如果用数组,需要复制;交集,还要判断是否相同。
并查集只支持合并和查询,不支持删除,更新,差集,补集,相交,求对称差,取反。
并查集讲解:blibli>>并查集>>正月点灯笼。
一开始,每个元素自己成为一个集合。1和2是亲戚,1、2的亲戚互相也是亲戚,合并,即把1、2中的一个作为另一个的孩子。用树来存。
2所在的集合的代表元素是这棵树的根节点1。
问:1、2是否为亲戚?
答:1所在集合的代表元素是1, 2所在集合的代表元素也是1,两者代表元素相同,所以1、2是亲戚。
1、2:把2挂到1(或把1挂到2);
1、5:把5挂到1(或把1挂到5);
3、4:把4挂到3(或把3挂到4);
5、2:5和2已在同一集合(集合代表元素都是1),不必操作。
1、3:把3挂到1(或把1挂到3)。
双亲的表示方法:用数组,存每个元素的父亲是谁,无父亲,用-1表示,根的父亲用-1表示。
查询:找根。
合并:输入a、b是亲戚,若a、b已属于同一集合,不管,若a、b不属于同一集合,把b挂到a(或把a挂到b)。
import java.util.ArrayList;
import java.util.Scanner;public class P1569UFS {
int[] Parent;
int n,m,T;
int Find(int a) {//寻找a所在集合,根是谁
while(Parent[a]!=-1) a=Parent[a];
return a;//a就是根节点
}
public P1569UFS() {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();m=sc.nextInt();T=sc.nextInt();
Parent=new int[n+1];
for(int i=1;i<=n;i++) Parent[i]=-1;//单独n棵树
for(int i=0;i<m;i++) {
int x=sc.nextInt(),y=sc.nextInt();
int SETX=Find(x),SETY=Find(y);
if(SETX!=SETY) Parent[SETY]=SETX;//合并两个集合
}
for(int i=0;i<T;i++) {
int x=sc.nextInt(),y=sc.nextInt();
System.out.println(Find(x)==Find(y) ? "Yes":"No");
}
}
public static void main(String[] args) {
P1569UFS p=new P1569UFS();
}}
并查集改进--路径压缩:
树越挂越高,树很高,离根较远的结点找根时一步步走,非常慢。极端的情况是一条线挂下来,查询时间为n-1,平均查询时间为n/2。
查a的时候,把a沿路走过的全指向1,不让a白走,下次查3、4时,一次即可查到,这样,随着查询次数的增多,树会越来越矮。
import java.util.ArrayList;
import java.util.Scanner;public class P1569UFS_PC {
int[] Parent;
int n,m,T;
int Find(int a) {//寻找a所在集合,根是谁
if(Parent[a]==-1) return a;//如果是根节点,返回
Parent[a]=Find(Parent[a]);//如果不是根节点,走到其父亲;返回根节点,把当前节点的父亲指向根节点
return Parent[a];
}
public P1569UFS_PC() {
Scanner sc=new Scanner(System.in);
n=sc.nextInt();m=sc.nextInt();T=sc.nextInt();
Parent=new int[n+1];
for(int i=1;i<=n;i++) Parent[i]=-1;//单独n棵树
for(int i=0;i<m;i++) {
int x=sc.nextInt(),y=sc.nextInt();
int SETX=Find(x),SETY=Find(y);
if(SETX!=SETY) Parent[SETY]=SETX;//合并两个集合
}
for(int i=0;i<T;i++) {
int x=sc.nextInt(),y=sc.nextInt();
System.out.println(Find(x)==Find(y) ? "Yes":"No");
}
}
public static void main(String[] args) {
P1569UFS_PC p=new P1569UFS_PC();
}}
运行时间缩短,但不是很明显,这是因为此题的瓶颈在输入。