并查集是个很实用的数据结构,主要功能可以动态维护若干个不重叠的集合,并支持合并与查询,当然这些都只是概念,个人理解并查集能够维护具有传递性的关系,还可有维护节点之间的连续性,克鲁斯卡尔就是这样做到的.普通的并查集就是如此,今天介绍并查集的扩展,看题:
题目一看便知道要用到并查集,判断在不在一列可以普通的并查集就能实现,可是如何实现了解两艘战舰之间的距离呢?
我刚开始也是直接暴力枚举,加上一个son数组和dis数组,dis表示点到点所在树的根的距离,每次合并两个树时,暴力修改。
最后辛辛苦苦把代码写完,就得了50,以下是暴力代码:
#include<bits/stdc++.h> using namespace std; const int maxn=31000; int f[maxn],t,son[maxn],dis[maxn]; char ch; inline int read() { int x=0,ff=1; char ch=getchar(); while(!isdigit(ch)) { if(ch=='-') ff=-1; ch=getchar(); } while(isdigit(ch)) { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*ff; } inline void put(int x) { if(x<0) putchar('-'),x=-x; if(x>9) put(x/10); putchar(x%10+'0'); } inline int getf(int x) { if(f[x]==x) return x; else return (getf(f[x])); } inline int gets(int x) { if(son[x]==x) return x; else return (gets(son[x])); } inline void work1(int tou,int wei) { f[tou]=wei;son[wei]=tou; int i=tou; while(1) { if(i==son[i]) {dis[i]=dis[f[i]]+1;break;} dis[i]=dis[f[i]]+1; i=son[i]; } } int main() { //freopen("1.in","r",stdin); t=read(); for(int i=1;i<=30000;i++) f[i]=i,son[i]=i; for(int i=1;i<=t;i++) { cin>>ch; int x=read(),y=read(); int t1=getf(x),t2=gets(x); int t3=getf(y),t4=gets(y); if(ch=='M') work1(t1,t4); if(ch=='C') { if(t1==t3) cout<<abs(dis[x]-dis[y])-1<<endl; else cout<<-1<<endl; } } return 0; }
实在不行就去看了算法进阶,才知到不需维护各个点到根的距离,那样复杂度太高,可以维护每个点与它f[x]的权值(也可理解为从根开始的编号,根不算在内),为什么呢,因为这样你在找父亲与路径压缩时就可以顺便把点x到根的距离解决,因为每次路径压缩都是将路上各点单独拿出来与根相连,距离也就可以解决。找父亲代码:
inline int getf(int x) { if(f[x]==x) return x; //找到根,返回。 int root=getf(f[x]); //找根。 dis[x]+=dis[f[x]]; //因为此时经过回溯,dis[f[x]]的距离已经是 f[x]到根的距离了,所以 return f[x]=root; //dis[x]只需将原本与父亲相连的权值加上父亲的距离即是与跟的距离。 } //最后加上路径压缩.
解决了找父亲,接下来就是合并了,两个子树合并,需要求出x的到y的距离(x,y为两个子树的根,且x要到y上),显然,dis[s]便是y的size(),所以还需维护以个size()数组表示各个集合的点数量,
代码:
inline void merge(int x,int y) { f[x]=y; dis[x]=size[y]; size[y]+=size[x]; }
把这些解决了,题目也就没那么难了,总代码:
#include<bits/stdc++.h> using namespace std; const int maxn=31000; int f[maxn],t,dis[maxn],size[maxn]; char ch; inline int read() { int x=0,ff=1; char ch=getchar(); while(!isdigit(ch)) { if(ch=='-') ff=-1; ch=getchar(); } while(isdigit(ch)) { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*ff; } inline void put(int x) { if(x<0) putchar('-'),x=-x; if(x>9) put(x/10); putchar(x%10+'0'); } inline int getf(int x) { if(f[x]==x) return x; //找到根,返回。 int root=getf(f[x]); //找根。 dis[x]+=dis[f[x]]; //因为此时经过回溯,dis[f[x]]的距离已经是 f[x]到根的距离了,所以 return f[x]=root; //dis[x]只需将原本与父亲相连的权值加上父亲的距离即是与跟的距离。 } //最后加上路径压缩. inline void merge(int x,int y) { f[x]=y; dis[x]=size[y]; size[y]+=size[x]; } int main() { freopen("1.in","r",stdin); t=read(); for(int i=1;i<=30000;i++) f[i]=i,size[i]=1; for(int i=1;i<=t;i++) { cin>>ch; int x=read(),y=read(); int t1=getf(x); int t2=getf(y); if(ch=='M') merge(t1,t2); if(ch=='C') { if(t1==t2) put(abs(dis[x]-dis[y])-1),cout<<endl; else put(-1),cout<<endl; } } return 0; }
感觉这个题才让我懂得了并查集的真正含义:并查集能够确定记录并合并两者的关系.
比如说这道题,通过简单的分析我们知道,开着的灯的两个开关必须总和为偶数次,才能保证这个灯继续开着.而闭着的灯必须总和为奇数次才能把灯打开...
这就要需要我们记录两个开关的和为奇偶关系,这是我们把每个开关拆成两个点,奇数和偶数即可.
#include<bits/stdc++.h> using namespace std; const int N=2e5+10; int f[N<<1],n,m,a[N];//1倍表示奇数次,2表示偶数次. vector<int>v[N]; inline int read() { int x=0,ff=1; char ch=getchar(); while(!isdigit(ch)) {if(ch=='-') ff=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*ff; } inline int getf(int k){return k==f[k]?k:f[k]=getf(f[k]);} int main() { freopen("1.in","r",stdin); n=read();m=read(); for(int i=1;i<=n;++i) a[i]=read(); for(int i=1;i<=m;++i) { int x=read(); for(int j=1;j<=x;++j) { int c=read(); v[c].push_back(i); } } for(int i=1;i<=m<<1;++i) f[i]=i; for(int i=1;i<=n;++i) { int t1=getf(v[i][0]);//左奇 int t2=getf(v[i][0]+m);//左偶 int t3=getf(v[i][1]);//右奇 int t4=getf(v[i][1]+m);//右偶 if(!a[i]) { if(t1==t3) {printf("NO");return 0;} else f[t1]=t4,f[t2]=t3; } else { if(t1==t4) {printf("NO");return 0;} else f[t1]=t3,f[t2]=t4; } } printf("YES"); return 0; }