//本文首先发表于博客园,点击这里查看我的博客。
废话不多说,直接看题:
一看这道题,我就有了思路:既然这道题身在图论板块,那么就要用图的存储、操作方法来解决,先开一个二维数组a[20001][20001],把初值尽可能赋大,再输入数据,并建立关系,然后用floyed算法,虽然不用求最短路径,但是至少能知道两人的关系能否通过中继联通,如果结果正常(即a[i][j]!=999999),则输出“Yes”,否则输出“No”。
代码如下:
<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">#include<iostream>
using namespace std; int a[20001][20001],n,m,q;int x,y; int main()
{
cin>>n>>m; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
{
a[i][j]=999999;//初始化,方便floyed算法
} for(int i=1;i<=n;i++)
a[i][i]=0;//自己和自己当然不是亲戚
for(int i=1;i<=m;i++)
{
cin>>x>>y;
a[x][y]=1;//建立关系
a[y][x]=1;
} for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
{ if(a[i][j]>a[i][k]+a[k][j])//floyed算法
a[i][j]=a[i][k]+a[k][j];
}
cin>>q; while(q--)
{
cin>>x>>y; if(a[x][y]!=999999)
cout<<"Yes"<<endl;//有关系
else cout<<"No"<<endl;//无关系
} return 0;
}</pre>
可是结果却不容乐观,花式错误:运行超时,运行错误,内存超限。通过这道题我明白了电脑内存有多么脆弱,才410^8个数,我平时十分珍惜空间大小,每一次都开小数组,难得浪一次,竟然爆空间了。我仍不死心,又换成深搜试了试,结果连样例都过不了~~,代码就不奉上了;后来我有恶补比floyed算法快的其他最短路径算法(如dijkstra),发现时间复杂度也好不到哪去。后来回头一看题,竟然关系就有100,0000个,询问有10,0000次,这是让我O(n)做完吗,还要用一维数组*,后来听说要用并查集。
那么并查集是什么?这并查集得分开看每一个字;前两个字“并”和“查”都是并查集的基本操作,稍后自然会说;“集”则代表这些数是一个个集合。“天生我才必有用”,并查集比起数组来说,更节省空间,二维数组10000*10000就爆了,怎能容得下这道题的数据范围呢?而并查集用的只是一维数组;而且并查集比起floyed算法来说,时间复杂度也只有O(n),比floyed O(n^3)好多了。
接下来就以此题为例,介绍一下并查集的基本操作:
1)初始化并查集:把每一个数据都先初始成一个单独的集合;
2)查找:不断递归/循环找到指定数据的父节点,直到找到其祖先节点,其中并查集数组每一元素存储的是其父节点编号,祖先节点满足无父节点,也就是其存储值仍为初始化时的值,即其编号等于其存储的值(因为初始化时为了分成不同集合把存储值赋成其编号);祖先节点并非真的是祖先,其实就是一个标志,只要值为同一祖先节点编号的元素,都能判断他们在同一集合中,节省了时间;
3)合并:如果两人祖先不同,且数据给出他们有亲缘关系,就可以将他们合并到同一祖先下;
4)判断元素是否在同一集合中:判断二人祖先是否相同,如果相同,则在同一集合内;否则在不同集合。
废话不多说,为了更好理解,特呈上手写模板,可以与上文结合看:
<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">#include<iostream>
using namespace std; int a[20001],n,m,x,y,q,x2,y2; void setup()//初始化并查集
{ for(int i=1;i<=n;i++)//共有n个人,每个人都要初始化
{
a[i]=i;//初始化每个人都是独立的集合
}
} int find(int x)//查找祖先
{ if(a[x]==x) return x;//找到祖先立即返回
else return a[x]=find(a[x]);//否则继续寻找+优化(路径压缩)
} void mix(int x,int y)//合并至同一集合
{
x=find(x);y=find(y);//计算两人的祖先
if(x!=y) a[x]=y;//若不相同则将x合并到y的祖先下
} bool is_relative(int x,int y)//判断是否有亲属关系
{ return find(x)==find(y);//查看两人祖先,判断是否相同并返回布尔值以便输出
} int main()
{
cin>>n>>m;
setup();//初始化
for(int i=1;i<=m;i++)
{
cin>>x2>>y2;
mix(x2,y2);//合并到同一集合
}
cin>>q; while(q--)
{
cin>>x2>>y2; if(is_relative(x2,y2)) cout<<"Yes"<<endl;//有关系
else cout<<"No"<<endl;//没关系
} return 0;
}</pre>
要AC代码的同志切勿粘贴,别说我没说过,这只是个模板。小编习惯用cin,cout再加上多次使用函数,会使速度变慢,但是改过之后发现仍不能过,代码就算了,于是又研究并查集的优化方法,提升速度,常规优化方法如下:
1)路径压缩:听到这个词,小编就想路径压缩麻烦吗?会有什么好处?既然压缩,又怎么压缩?不用担心,小编会一一解答。其实小编代码中的find函数已经在用路径压缩了,可以和我下面讲的相结合来看。其实也好理解,下面是小编一时兴起画的,技术不好,见谅。
如图所示左图为路径压缩前的亲戚关系图,要确定4和6有无亲戚关系则需要浪费更多的时间;但右图是压缩路径后的亲戚关系图,能很好地表现出各节点到祖先节点的关系,查找次数也由此减少。但路径压缩的弊端也显露出来,这样虽然能表现出各节点和祖先节点的关系,但却破坏了原本的结构,无法判断各节点到父节点的直接关系,在个别题目不能使用。
2)按秩合并:这个优化方法既麻烦又不怎么常见,有两种,按大小合并和按深度合并,大小合并很简单,小的合到大的集合中呗;深度合并则考虑的更多,将关系当成树,把深度小的合并到深度大的上。优点是既能节约时间还能保留好原有关系,但是没有路径压缩快,此题不宜用,也就不详解了(其实是小编不太会),知道思想就行。
优化之后就大功告成了,小编心中尚存一丝侥幸,据说“std::ios::sync_with_stdio(false);可以使cin,cout和scanf,printf速度相差无几”,这是在《信息学奥赛一本通》上看到的,应该挺有用的吧,小编试了一下,切,还相差无几,简直和原来一样,依旧是四个测试点没过超时,最终全部改成scanf,printf就通过了。行了,不说了,代码胜于雄辩,AC代码呈上:
<pre style="color: rgb(0, 0, 0); font-family: "Courier New"; font-size: 12px; margin: 5px 8px; padding: 5px;">#include<cstdio>
using namespace std; int a[20001],n,m,sum,p,q; /void setup() //初始化每一个集合
{
for(int i=1;i<=n;i++)
{
a[i]=i;//每一个人都初始化为一个单独集合
}
}/
int find(int x)//不断寻找父节点+路径压缩
{ if(a[x]==x) return x;//找到尽头,即祖先节点
else return find(a[x]);
} /void mix(int x,int y)//合并各个集合
{
x=find(x);y=find(y);
if(x!=y) a[x]=y;//将x和y合并到同一祖先下
}/
/bool is_relative(int x,int y)//判断是否同一祖先,是否在同一集合
{
return find(x)==find(y);
}/
int main()
{
scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)
{
a[i]=i;//每一个人都初始化为一个单独集合
} for(int i=1;i<=m;i++)
{
scanf("%d%d",&p,&q); int r1=find(p); int r2=find(q); if(r1!=r2) a[r2]=r1;
}
scanf("%d",&sum); while(sum--)
{ int x,y;
scanf("%d%d",&x,&y); if(find(x)==find(y)) printf("Yes \n"); else printf("No \n");
} return 0;
}</pre>