2021年hznu寒假集训第二天
关键词:代表元
解决问题:问题的朴素解法
概念:并查集主要记录节点之间的链接关系,而没有其他的具体的信息,仅仅代表某个节点与其父节点之间存在联系,它多用来判断图的连通性,如下图所示,这是一个并查集,其中箭头表示父子关系。
优化
路径压缩
我们通常把路径优化后的结果叫做菊花图.
即将上面的图变成下面的
接下来我们来介绍一下路径压缩.
C的父节点是B,而B的父节点是A,所以我们就可以把A设为C的父节点。
接下来看一下代码:
int find(int x)
{
return x==parent[x]?parent[x]:parent[x]=find(parent[x]);
}
合并
比如这题,我们想要证明环存在,进行完路径压缩,我们要合并。
合并就是将两个集变成一个集
如图所示,0、2的父节点是1, 3的父节点是4,要想这两个集有关联,我们要尝试将他们的父节点相关联,例如这样
那这个过程我们就把他叫做合并。
下面是它的代码:
void Union(int x,int y){
int fx=find(x),fy=find(y);
//找出父节点
if(fx!=fy)
//证明父节点不相同,即他们是在两个集合里面
parent[fx]=fy;
//则将其中一个父节点y设为另一个父节点x的父节点
}
下面我们来解决一下上面证明环存在的问题
#include<stdio.h>
#include<stdlib.h>
#define VERTICES 6
void initialise(int parent[]){
int i;
for(i=0;i<VERTICES;i++){
parent[i]=-1;
}
}
int find_root(int x,int parent[]){
int x_root=x;
while(parent[x_root]!=-1){
x_root=parent[x_root];
}
return x_root;
}
/*1- union successfully,0-failed*/
int union_vertices(int x,int y,int parent[]){
int x_root=find_root(x,parent);
int y_root=find_root(y,parent);
if(x_root==y_root){
return 0;
}
else{
parent[x_root]=y_root;
return 1;
}
}
int main(){
int parent[VERTICES]={0};
int edges[6][2]={
{0,1},{1,2},{1,3},
{2,4},{3,4},{2,5}
};
initialise(parent);
int i;
for(i=0;i<6;i++){
int x=edge[i][0];
int y=edge[i][1];
if(union_vertices(x,y,parent)==0){
printf("Cycle detected!\n");
exit(0);
}
}
printf("NO cycles found.\n");
return 0;
}
但是这串代码有一个缺陷,如果我们的图是
比如第一次union(0,1),第二次(1,2),第三次(2,3)……
这个时候,会形成一个特别长的链,在查找parent的时候就会执行特别多次,时间复杂度是log n级别的
所以我们这个时候要再进行优化。
按秩合并
首先我们要引入秩的概念。
rank:指代树的高度
这棵树x的高度是3;
而这棵树y的高度则是1;
如果我们把树y接在树x上,即x作为y的parent,那么,他的rank并不发生改变;
反过来,他的rank变成了4;
后者显然是我们不希望的,所以我们要按秩合并。
if rank[x]>rank[y]
parent[y]=x;
//rank 不变->x
else
parent[x]=y;
之前那道题的代码就可以优化了
#include<stdio.h>
#include<stdlib.h>
#define VERTICES 6
void initialise(int parent[],int rank[]){
int i;
for(i=0;i<VERTICES;i++){
parent[i]=-1;
rank[i]=0;
}
}
int find_root(int x,int parent[]){
int x_root=x;
while(parent[x_root]!=-1){
x_root=parent[x_root];
}
return x_root;
}
/*1- union successfully,0-failed*/
int union_vertices(int x,int y,int parent[],int rank[]){
int x_root=find_root(x,parent);
int y_root=find_root(y,parent);
if(x_root==y_root){
return 0;
}
else{
//parent[x_root]=y_root;
if(rank[x_root]>rank[y_root]){
parent[y_root]=x_root;
}
else{
parent[x_root]=y_root;
}
return 1;
}
}
int main(){
int parent[VERTICES]={0};
int rank[VERTICES]={0};
int edges[6][2]={
{0,1},{1,2},{1,3},
{2,4},{3,4},{2,5}
};
initialise(parent,rank);
int i;
for(i=0;i<6;i++){
int x=edge[i][0];
int y=edge[i][1];
if(union_vertices(x,y,parent,rank)==0){
printf("Cycle detected!\n");
exit(0);
}
}
printf("NO cycles found.\n");
return 0;
}
应用
最小生成树的构成
维护无向图连通性
拓展
带权并查集
种族并查集
种族并查集,就是把同一个集合中的每个元素赋予多个不同的属性,在不同属性的对应元素间建立关系,关系与关系之间能够虽然有冗余,但是互相联系能够很快得出两个元素之间的关系。
经典题目:
团伙
关押罪犯
食物链
三者做法基本相同,取其中食物链讲一下。
常规法:
1.p[x]表示x根结点。r[x]表示p[x]与x关系。r[x]=0 表示p[x]与x同类;1表示p[x]吃x;2表示x吃p[x]。
2.怎样划分一个集合呢?
注意,这里不是根据x与p[x]是否是同类来划分。而是根据“x与p[x]能否确定两者之间关系”来划分,若能确定x与p[x]关系,则它们同属一个集合
3.3.怎样判断一句话是不是假话?
假设已读入D ,X ,Y ,先利用findset()函数得到X,Y所在集合代表元素fx,fy,若它们在同一集合(即fx=fy)则可以判断这句话真伪:
若 D=1 而 r[X]!=r[Y] 则此话为假.(D=1 表示X与Y为同类,而从r[X]!=r[Y]可以推出 X 与 Y 不同类.矛盾.)
若 D=2 而 r[X]=r[Y](X与Y为同类)或者r[X]=(r[Y]+1)%3(Y吃X)则此话为假。
4.上个问题中r[X]=(r[Y]+1)%3这个式子怎样推来?
假设有Y吃X,那么r[X]和r[Y]值是怎样?
我们来列举一下:
r[X]=0&&r[Y]=2
r[X]=1&&r[Y]=0
r[X]=2&&r[Y]=1
稍微观察一下就知道r[X]=(r[Y]+1)%3;
事实上,对于上个问题有更一般判断方法:
若(r[Y]-r[X]+3)%3!=D-1 ,则此话为假.
5.其他注意事项:
在Union(d,x,y)过程中若将S(fy)合并到S(fx)上,则相应r[fy]必须更新为fy相对于fx关系。怎样得到更新关系式?
r[fy]=(r[x]-r[y]+d+3)%3;
#include<cstdio>
const int N=50001;
int p[N],r[N],n;
int findset(int x)
{
if(x!=p[x])
{
int fx=findset(p[x]);
r[x]=(r[x]+r[p[x]])%3;
p[x]=fx;
}
return p[x];
}
bool Union(int d,int x,int y)
{
int fx=findset(x),fy=findset(y);
if(fx==fy)
{
if((r[y]-r[x]+3)%3!=d)return 1;
else return 0;
}
p[fy]=fx;
r[fy]=(r[x]-r[y]+d+3)%3;
return 0;
}
int main()
{
int k,ans,i,d,x,y;
scanf("%d%d",&n,&k);
ans=0;
for(i=1;i<=n;i++)p[i]=i,r[i]=0;
while(k--)
{
scanf("%d%d%d",&d,&x,&y);
if(x>n||y>n||(x==y&&d==2)){ans++;continue;}
if(Union(d-1,x,y))ans++;
}
printf("%d\n",ans);
return 0;
}