并查集原来以前看过,当时看的那篇文章编了一个江湖的关系,令我印象深刻,可惜我找不到了。
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示。
基础
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,m;
int z,x,y;
int f[maxn];
void init(){
for(int i=1;i<=n;i++)
f[i]=i;
}
int getFriend(int x){
if(f[x]==x)
return x;
return f[x]=getFriend(f[x]);
}
void merge(int x,int y){
int t1=getFriend(x);
int t2=getFriend(y);
if(t1!=t2){
f[t2]=t1; //靠左原则
}
}
int main(){
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++){
cin>>z>>x>>y;
if(z==1){
merge(x,y);
}
else{
if(getFriend(x)==getFriend(y)){
cout<<"Y"<<endl;
}
else
cout<<"N"<<endl;
}
}
return 0;
}
优化
靠左原则或者靠右原则还可以进行优化,所以我们改变合并两个朋友圈的方式,变成“比较朋友圈高度原则”。(按秩合并)
将高度较小的朋友圈合并到高度较大的朋友圈里,合并完后调整高度即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,m;
int z,x,y;
int f[maxn];
int h[maxn];
void init(){
for(int i=1;i<=n;i++)
f[i]=i;
}
int getFriend(int x){
if(f[x]==x)
return x;
return f[x]=getFriend(f[x]);
}
void merge(int x,int y){
int t1=getFriend(x);
int t2=getFriend(y);
if(t1!=t2){
if(h[t2]>h[t1])
f[t2]=t1;
else{
f[t1]=t2;
if(h[t2]==h[t1])
h[t1]++;
}
}
}
int main(){
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++){
cin>>z>>x>>y;
if(z==1){
merge(x,y);
}
else{
if(getFriend(x)==getFriend(y)){
cout<<"Y"<<endl;
}
else
cout<<"N"<<endl;
}
}
return 0;
}
种类并查集
(1)并查集可以维护连通性、传递性,通俗的说朋友的朋友是朋友,
然而我们还需要维护一些对立的关系,比如敌人的敌人是朋友,所以正常的并查集很难满足我们的需求,所以种类并查集诞生。
(2)常见的种类并查集是将原来的并查集扩大一倍规模,并划分为两个种类。
在同个种类的并查集合并,代表他们是朋友,在不同种的并查集,他们就是敌人。
(3)按照并查集的传递性,我们就能具体知道两个是元素是朋友还是敌人。
(4)至于某个元素到底属于两个种类中的哪一个,由于我们不清楚,因此两个种类我们都试试。
A Bug’s Life
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
bool ok;
int f[maxn*2],t,n,m;
void init(){
for(int i=1;i<=2*n;i++){
f[i]=i;
}
}
int find(int x){
if(f[x]==x)
return x;
return x=find(f[x]);
}
void merge(int x,int y){
int t1=find(x);
int t2=find(y);
if(t1!=t2){
f[t2]=t1;
}
}
int main(){
cin>>t;
for(int i=1;i<=t;i++){
cin>>n>>m;
init();
ok=false;
for(int j=1;j<=m;j++){
int x,y;
cin>>x>>y;
if(find(x)!=find(y)){
merge(x+n,y);
merge(x,y+n);
}
else{
ok=true;
}
}
if(ok)
printf("Scenario #%d:\nSuspicious bugs found!\n\n",i);
else
printf("Scenario #%d:\nNo suspicious bugs found!\n\n",i);
}
}
再上一种
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
int t,n,m,f[maxn],b[maxn],x,y;
bool ok;
void init(){
memset(b,0,sizeof(b));
for(int i=1;i<=maxn;i++)
f[i]=i;
}
int find(int x){
if(f[x]==x)
return x;
return x=find(f[x]);
}
void merge(int x,int y){
int t1=find(x);
int t2=find(y);
if(t1!=t2){
f[t1]=t2;
}
}
int main(){
scanf("%d",&t);
for(int i=1;i<=t;i++){
scanf("%d%d",&n,&m);
ok=false;
init();
while(m--){
scanf("%d%d",&x,&y);
if(find(x)!=find(y)){
if(!b[x])b[x]=y;
else merge(b[x],y);
if(!b[y])b[y]=x;
else merge(b[y],x);
}
else ok=true;
}
if(ok)printf("Scenario #%d:\nSuspicious bugs found!\n\n",i);
else printf("Scenario #%d:\nNo suspicious bugs found!\n\n",i);
}
return 0;
}
关押罪犯
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
struct node{int x,y;ll c;}s[maxn];
int n,m;
int b[maxn];
int f[maxn];
int h[maxn];
bool cmp(const node&a,const node&b){
return a.c>b.c;
}
void init(){
for(int i=1;i<=n;i++)
f[i]=i;
return;
}
int find(int x){
if(f[x]==x)
return x;
return x=find(f[x]);
}
void merge(int a,int b){
int t1=find(a);
int t2=find(b);
if(h[t1]>h[t2])
f[t2]=t1;
else{
f[t1]=t2;
if(h[t1]==h[t2])
h[t1]++;
}
}
int main(){
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++)
scanf("%d%d%lld",&s[i].x,&s[i].y,&s[i].c);
sort(s+1,s+1+m,cmp);
for(int i=1;i<=m+1;i++){//m+1是为了输出0的情况
if(find(s[i].x)==find(s[i].y)){
printf("%lld\n",s[i].c);
break;
}
if(!b[s[i].x])b[s[i].x]=s[i].y;
else merge(b[s[i].x],s[i].y);
if(!b[s[i].y])b[s[i].y]=s[i].x;
else merge(b[s[i].y],s[i].x);
}
return 0;
}
或者另一种写法与食物链类似的一种写法
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10;
int n,m;
int h[maxn],f[maxn];
struct node{int x,y;ll c;}s[maxn];
bool cmp(const node&a,const node&b){
return a.c>b.c;
}
void init(){
for(int i=1;i<=n*2;i++)
f[i]=i;
}
int find(int x){
if(f[x]==x)
return x;
return x=find(f[x]);
}
void merge(int a,int b){
int t1=find(a);
int t2=find(b);
if(h[t2]>h[t1])
f[t2]=t1;
else{
f[t1]=t2;
if(h[t1]==h[t2])
h[t1]++;
}
}
int main(){
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++){
scanf("%d%d%lld",&s[i].x,&s[i].y,&s[i].c);
}
sort(s+1,s+1+m,cmp);
for(int i=1;i<=m+1;i++){
if(find(s[i].x)==find(s[i].y))
{
printf("%lld\n",s[i].c);
break;
}
else{
merge(f[s[i].x+n],f[s[i].y]);
merge(f[s[i].x],f[s[i].y+n]);
}
}
return 0;
}
注意事项:种类并查集求的并不是具体种类,而是关系
3倍并查集。
x元素:[1,n]是同类
[n+1,2n]是猎物
[2n+1,3n]是天敌
感觉关系是循环的
食物链
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int s[maxn*3],f[maxn*3],h[maxn*3];
int n,k;
int sum;
void init(){
for(int i=1;i<=n*3;i++){
f[i]=i;
}
return;
}
int find(int x){
if(f[x]==x)
return x;
return x=find(f[x]);
}
void merge(int a,int b){
int t1=find(a);
int t2=find(b);
if(h[t1]>h[t2])
f[t2]=t1;
else{
f[t1]=t2;
if(h[t1]==h[t2])
h[t1]++;
}
}
int main(){
scanf("%d%d",&n,&k);
init();
for(int i=1;i<=k;i++){
int opt,x,y;
scanf("%d%d%d",&opt,&x,&y);
if(x>n||y>n){
sum++;
continue;
}
if(opt==1){//x与y同类
if(x==y)
continue;
if(find(x+n)==find(y)||find(x+2*n)==find(y))
sum++;
else{
merge(x,y);
merge(x+n,y+n);
merge(x+2*n,y+2*n);
}
}
else{//x吃y
if(x==y||find(x)==find(y)||find(x+2*n)==find(y))
sum++;
else{
merge(x+n,y);
merge(x+n*2,y+n);
merge(x,y+2*n);
}
}
}
cout<<sum<<endl;
return 0;
}
带权并查集
带权并查集即是结点存有权值信息的并查集;带权并查集每个元素的权通常描述其与并查集中祖先的关系,这种关系如何合并,路径压缩时就如何压缩;带权并查集可以推算集合内点的关系,而一般并查集只能判断属于某个集合。
要考虑压缩路径时的维护关系,我们压缩路径时知道A和B的关系,知道A和A根结点的关系,需要推导B与A根结点的关系
权值具体是什么要根据题意,一般都是两个节点之间的某一种相对的关系
带权并查集的权值,要随着压缩路径的过程进行更新
A Bug’s Life
传送门
学了一天的带权并查集,终于稍微懂点了。
这道题比关押罪犯简单,与食物链类似但还是简单,因为这道题就两个关系,一个就是同性、一个就是异性,分别用0和1表示。
(1)首先是合并考虑压缩路径时的关系维护,我们压缩路径时已知B和A的关系,以及A和A根节点的关系,需要推导出B和A根节点的关系
A与根结点的关系 | B与A的关系 | B与A根结点的关系 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
int find(int x){
if(x==f[x])
return x;
int t=find(f[x]);//找到父亲节点的根结点
v[x]=(v[x]+v[f[x]])%2;//更新权值
f[x]=t;//更新x的父亲节点
return f[x];
}
(2) 然后我们考虑关系的查找,我们以及知道A和B在同一集合,即代表他们根节点相同,我们要确定两者之间的关系,根据这题,如果A与B是1的话,符合题意,A与B是0的话,就是可疑缺陷发现。
A与根结点的关系 | B与根结点的关系 | A与B的关系 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
if(t1==t2){
if((v[a]+v[b])%2==0)
return true;
else return false;
}
(3) 最后我们考虑合并两个节点时关系的维护,我们已经知a和其根节点的关系,以及b和其根节点的关系,当我们把b集合合并到a集合时,我们需要考虑b根节点和a根节点存在的关系
也是根据表格的关系得出代码
最后注意一下,发现有缺陷后,不能马上break,因为数据还没有输入完
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=2e3+10;
int n,m,t;
bool ok;
int f[maxn],v[maxn];
void init(){
for(int i=1;i<=maxn;i++){
f[i]=i;
v[i]=0;
}
}
int find(int x){
if(x==f[x])
return x;
int t=find(f[x]);//root
v[x]=(v[x]+v[f[x]])%2;
f[x]=t;
return f[x];
}
bool merge(int a,int b){
int t1=find(a);
int t2=find(b);
if(t1==t2){
if((v[a]+v[b])%2==0)
return true;
else return false;
}
else{
f[t1]=t2;
v[t1]=(1+v[a]+v[b])%2;
}
return false;
}
int main(){
cin>>t;
for(int i=1;i<=t;i++){
ok=false;
scanf("%d%d",&n,&m);
init();
for(int j=1;j<=m;j++){
int x,y;
scanf("%d%d",&x,&y);
if(merge(x,y)){
ok=true;//不能break
}
}
if(ok){
printf("Scenario #%d:\nSuspicious bugs found!\n\n",i);
}
else{
printf("Scenario #%d:\nNo suspicious bugs found!\n\n",i);
}
}
return 0;
}
食物链
传送门
让我们再谈食物链这道题
我们需要创建pre数组和rela数组,pre数组判断集合之间的关系,rela判断集合内部的关系。这题我们建立三种关系:X与Y是同类、X是Y的猎物(Y吃X)、X是Y的天敌(X吃Y),在rela数组分别用0,1,2表示。
A与其根结点的关系 | B与A的关系 | B与A的根结点之间的关系 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
0 | 2 | 2 |
1 | 0 | 1 |
1 | 1 | 2 |
1 | 2 | 0 |
2 | 0 | 2 |
2 | 1 | 0 |
2 | 2 | 1 |
所以说可以得到rela[b]=(rela[b]+rela[a])%3的压缩路径关系
int find(int x){
if(x==pre[x])
return x;
else{
int temp=pre[x];
pre[x]=find(pre[x]);
rela[x]=(rela[x]+rela[temp])%3;
}
return pre[x];
}
然后考虑关系的查找,A与B根结点相同,他们一定是在同一集合,我们要确定A、B两者的关系。
A与根结点 | B与根结点 | A与B的关系 |
---|---|---|
0 | 0 | 0 |
0 | 1 | 2 |
0 | 2 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
1 | 2 | 2 |
2 | 0 | 2 |
2 | 1 | 1 |
2 | 2 | 0 |
rela[a->b]=(rela[a]-rela[b]+3)%3;
if(find(a)==find(b)){
relation=(rela[a]-rela[b]+3)%3;
return relation==r;
}
最后考虑合并两个节点关系的维护
rela[pre[B]->pre[A]]=(rela[A]+rela[B->A]-rela[pre[B]]+3)%3
void merge(int x,int y,int r){
int t1=find(x);
int t2=find(y);
if(t1!=t2){
pre[t1]=pre[t2];
rela[t1]=(rela[y]-rela[x]+r+3)%3;
}
}
P1196 [NOI2002]银河英雄传说
传送门
只可能一次移动整个队列,并且是**把两个队列首尾相接合并成一个队列,不会出现把一个队列分开的情况,**因此,我们必须要找到一个可以一次操作合并两个队列的方法。
再来看下C指令:判断飞船i和飞船j是否在同一列,若在,则输出它们中间隔了多少艘飞船。我们先只看判断是否在同一列,由于每列一开始都只有一艘飞船,之后开始合并,结合刚刚分析过的M指令,很容易就想到要用并查集来实现。
两艘飞船之间的飞船数量,其实就是艘飞船之间的距离,那么,这就转换为了一个求距离的问题。看见多次求两个点的距离的问题,便想到用前缀和来实现
v[]是权值即当前点到船头的距离,d[i]表示以i为首的船的数量
以t1为首的船头与t2为首的船头合并,一开始的v[t1]等于0,所以要用d[]记录
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e4+10;
int t,f[maxn],v[maxn],d[maxn];
void init(){
for(int i=1;i<=maxn;i++){
f[i]=i;
d[i]=1;
}
}
int find(int x){
if(x==f[x])
return x;
int t=find(f[x]);
v[x]=v[x]+v[f[x]];
f[x]=t;
return f[x];
}
void merge(int a,int b){
int t1=find(a);
int t2=find(b);
f[t1]=t2; //将以t1为船首的队列指到以t2为船首的队列
v[t1]=d[t2];t1到t2的距离那就是t2原来的船数
d[t2]+=d[t1];//t2现在的船数等于t2的加t1的
}
int main(){
init();
scanf("%d",&t);
while(t--){
char c;int x,y;
cin>>c>>x>>y;
if(c=='M'){
merge(x,y);
}
else{
if(find(x)!=find(y))
printf("-1\n");
else{
printf("%d\n",abs(v[x]-v[y])-1);
}
}
}
return 0;
}
P2342 叠积木
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e4+10;
int n;
int f[maxn],v[maxn],d[maxn];
void init(){
for(int i=1;i<=maxn;i++)
{
f[i]=i;
d[i]=1;
}
}
int find(int x){
if(x==f[x])
return x;
int t=find(f[x]);
v[x]=v[x]+v[f[x]];
return f[x]=t;
}
void merge(int a,int b){
int t1=find(a);
int t2=find(b);
f[t2]=t1;
v[t2]=d[t1];
d[t1]+=d[t2];
}
int main(){
scanf("%d",&n);
init();
while(n--){
char opt;int x,y;
cin>>opt;
if(opt=='M'){
scanf("%d%d",&x,&y);
merge(x,y);
}
else{
scanf("%d",&x);
printf("%d\n",d[find(x)]-v[x]-1);
}
}
return 0;
}