普通并查集 链接
#include<bits/stdc++.h>
using namespace std;
int pre[50000+5];
int t=1;
int find(int x){
while(x!=pre[x]){
x=pre[x];
}
return x;
}
void solve(int n,int m){
for(int i=1;i<=n;i++){
pre[i]=i;
}
while(m--){
int a,b;
cin>>a>>b;
int x=find(a),y=find(b);
if(x==y){
continue;
}
else{
pre[y]=x;
n--;
}
}
cout<<"Case "<<t<<": "<<n<<endl;
}
int main(){
int n,m;
while(cin>>n>>m){
if(n==0&&m==0) return 0;
else{
solve(n,m);
t++;
}
}
}
普通的路径压缩
int find(int x){
if(x!=pre[x]){
pre[x]=find(pre[x]);
}
return pre[x];
}
带权并查集
带权值的路径压缩
val[x]指的是x->rootx的距离
int find(int x){
if(x!=pre[x]){
int t=pre[x];
pre[x]=find(pre[x]);
val[x]+=val[t];
}
return pre[x];
}
合并
现在是已知x所在的并查集根节点为px,y所在的并查集根节点为py,如果有了x、y之间的关系,要将px并到py上,如果不考虑权值直接修改parent就行了,但是现在是带权并查集,必须得求出px与py这条边的权值是多少,很显然x到py两条路径的权值之和应该相同,就不难得出上面代码所表达的更新式。但是需要注意并不是每个问题都是这样更新的,有时候可能会做取模之类的操作
给出区间和[1,4]为20,[3,4]为15,再给出[1,2]为30,显然这个[1,2]的值就有问题,它应该为20-15=5。 这个题目需要分析什么时候会产生矛盾,不妨就具体看看上面的这个矛盾的例子
在这里这个图是画不了的,因为使用的都是闭区间,闭区间会加上端点的值,这就提醒我们这个题在处理的时候应该将闭区间的某一端变成开区间,比如将[1,4]变成(0,4],将[3,4]变成(2,4],将[1,2]变成(0,2]
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
const int N =200010;
int father[N];
int sum[N]; ///记录当前结点到根结点的距离
int _find(int x){
if(x!=father[x]){
int t = father[x];
father[x] = _find(father[x]);
sum[x]+=sum[t];
}
return father[x];
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
for(int i=0;i<=n;i++){
father[i] = i;
sum[i] = 0;
}
int ans = 0;
while(m--){
int a,b,v;
scanf("%d%d%d",&a,&b,&v);
a--;
int roota = _find(a);
int rootb = _find(b);
if(roota==rootb){
if(sum[a]-sum[b]!=v) ans++; // a和b的root都是c, v+sum[b]=sum[a]
}
else{
father[roota] = rootb;
sum[roota] = -sum[a]+sum[b]+v; //sum[x]指的是 x->root[x]的距离
}
}
printf("%d\n",ans);
}
return 0;
}
种类并查集 链接
一般的并查集,维护的是具有连通性、传递性的关系,例如亲戚的亲戚是亲戚。但是,有时候,我们要维护另一种关系:敌人的敌人是朋友。种类并查集就是为了解决这个问题而诞生的。
#include<bits/stdc++.h>
using namespace std;
const int maxn=50000+5;
int pre[maxn*3+5];
int find(int x){
if(x!=pre[x]){
pre[x]=find(pre[x]);
}
return pre[x];
}
void unite(int x,int y){
x=find(x);
y=find(y);
pre[x]=y;
}
int main(){
int n,k;
cin>>n>>k;
for(int i=1;i<=3*n;i++){
pre[i]=i;
}
int ans=0;
while(k--){
int check,x,y;
cin>>check>>x>>y;
if(x<0||y<0||x>n||y>n){
ans++;
continue;
}
else if(check==2&&x==y){
ans++;
continue;
}
else{
if(check==1){
if(find(x)==find(y+n)||find(x)==find(y+2*n)){ //ͬʱÊÇA£¬Í¬Ê±ÊÇB£¬Í¬Ê±ÊÇC Çé¿öÊÇÒ»ÑùµÄÖ»ÒªÓÐÆäÖÐÒ»ÖÖ²»³ÉÁ¢È«²¿²»³ÉÁ¢ËùÒÔÔÚÕâÀïÖ»ÒªÅжÏxÊÇAµÄʱºò³É²»³ÉÁ¢¼´¿É
ans++;
continue;
}
else{
unite(x,y);
unite(x+n,y+n);
unite(x+2*n,y+2*n);
}
}
else{
if(find(x)==find(y)||find(x)==find(y+2*n)){
ans++;
continue;
}
else{
unite(x,y+n);
unite(x+n,y+2*n);
unite(x+2*n,y);
}
}
}
}
cout<<ans<<endl;
}
可持久化并查集
https://www.bilibili.com/video/BV1r4411A7b8?p=2&spm_id_from=pageDriver
开两个数组rootfa和rootdep分别代表当前节点管理的(l,r)和(l==r)时其对应的深度
需要注意的是可持久化并查集并不能使用路径压缩,假设a-b-c-d,如果改变了d,那么需要加入3个新版本的root节点,这样会mle。
所以使用启发式合并:
如果深度不同则将深度小的节点的根节的父亲改为深度大的节点的根节点,当前版本对应的深度等于上个版本深度
modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
rootdep[ver]=rootdep[ver-1];
如果深度相同,则将x节点的根节的父亲改为y节点的根节点,然后将y节点的深度加1
modify(1,n,rootfa[ver-1],rootfa[ver],x,y)
modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
https://www.luogu.com.cn/problem/P3402 板子题
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
struct node{
int l,r,val;
}hjt[maxn*40*2];
int n,m;
int cnt,rootfa[maxn],rootdep[maxn],tot;
void build(int l,int r,int&now){
now=++cnt;
if(l==r){
hjt[now].val=++tot; //tot代表的是对应的父亲
return;
}
int m=(l+r)>>1;
build(l,m,hjt[now].l);
build(m+1,r,hjt[now].r);
}
void modify(int l,int r,int ver,int&now,int pos,int num){
now=++cnt;
hjt[now]=hjt[ver];
if(l==r){
hjt[now].val=num;
return;
}
int m=(l+r)>>1;
if(m>=pos){
modify(l,m,hjt[ver].l,hjt[now].l,pos,num);
}
else{
modify(m+1,r,hjt[ver].r,hjt[now].r,pos,num);
}
}
int query(int l,int r,int now,int pos){
if(l==r){
return hjt[now].val;
}
int m=(l+r)>>1;
if(m>=pos){
return query(l,m,hjt[now].l,pos);
}
else{
return query(m+1,r,hjt[now].r,pos);
}
}
int find(int ver,int x){
int fx=query(1,n,rootfa[ver],x);
if(fx==x){
return x;
}
else{
return find(ver,fx);
}
}
void merge(int ver,int x,int y){
x=find(ver-1,x),y=find(ver-1,y);
if(x==y){
rootfa[ver]=rootfa[ver-1];
rootdep[ver]=rootdep[ver-1];
}
else{
int depx=query(1,n,rootdep[ver-1],x);
int depy=query(1,n,rootdep[ver-1],y);
if(depx<depy){
modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
rootdep[ver]=rootdep[ver-1];
}
else if(depx>depy){
modify(1,n,rootfa[ver-1],rootfa[ver],y,x);
rootdep[ver]=rootdep[ver-1];
}
else{
modify(1,n,rootfa[ver-1],rootfa[ver],x,y);
modify(1,n,rootdep[ver-1],rootdep[ver],y,depy+1);
}
}
}
int main(){
cin>>n>>m;
build(1,n,rootfa[0]);
for(int ver=1;ver<=m;ver++){
int opt;
cin>>opt;
if(opt==1){
int x,y;
cin>>x>>y;
merge(ver,x,y);
}
else if(opt==2){
int k;
cin>>k;
rootfa[ver]=rootfa[k]; //对应的是rootfa[k]作为根节点
rootdep[ver]=rootdep[k];
}
else{
int x,y;
cin>>x>>y;
rootfa[ver]=rootfa[ver-1];
rootdep[ver]=rootdep[ver-1];
int fx=find(ver,x);
int fy=find(ver,y);
if(fx==fy){
cout<<"1"<<"\n";
}
else{
cout<<"0"<<"\n";
}
}
}
}
技巧删除边操作:首先,查询的操作很明显带有自反对称和传递性,易想到用并查集来解决,但是并查集只有添加边和查询两种操作,故需要一些技巧,就是读入并记录所有的操作,从最后一条操作开始处理,一开始的图是给定的图删去后来需要销毁的边后的图,记录答案,当读到销毁操作时再把需销毁的边加上去,这样就可以回避销毁操作(倒着来事实上把销毁操作变成了加边的操作)。 ZOJ-3261
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
#include <iostream>
using namespace std;
typedef pair<int,int>pii;
int const N = 10000 + 10;
int n,m,x,y,Q;
int p[N],fa[N],sum[N],num[N],ans[5*N];
pii c[2*N];
struct Query
{
string s;
int a,b;
}q[5*N];
set<pii>st;
void Init(){
st.clear();
for(int i=0;i<=n-1;i++) fa[i] = i,num[i] = i;
for(int i=0;i<=n-1;i++){ //每个星星的能量
scanf("%d",&p[i]);
sum[i] = p[i];
}
scanf("%d",&m);
for(int i=0;i<m;i++){ //相连的星星
scanf("%d%d",&x,&y);
st.insert(make_pair(min(x,y),max(x,y))); //这里一定要注意统一!!
}
scanf("%d",&Q);
for(int i=0;i<Q;i++){ //询问
cin>>q[i].s;
if(q[i].s[0] == 'q') cin>>q[i].a;
else{
scanf("%d%d",&q[i].a,&q[i].b);
st.erase(make_pair(min(q[i].a,q[i].b),max(q[i].a,q[i].b)));
}
}
}
int find(int x){
return x == fa[x] ? x : (fa[x] = find(fa[x]));
}
void Union(int x,int y){
int fx = find(x), fy = find(y);
if(fx != fy){
fa[fx] = fy;
if(sum[fy] < sum[fx]){
sum[fy] = sum[fx];
num[fy] = num[fx];
}else if(sum[fy] == sum[fx]){
if(num[fx] < num[fy])
num[fy] = num[fx];
}
}
}
int main(){
bool first = true;
while(~scanf("%d",&n)){
if(first) first = false;
else printf("\n");
Init();
for(set<pii>::iterator it=st.begin();it!=st.end();it++)
Union(it->first,it->second);
for(int i=Q-1;i>=0;i--){
if(q[i].s[0] == 'q'){
int u = find(q[i].a);
if(sum[u] <= p[q[i].a]) ans[i] = -1;
else ans[i] = num[u];
}else{
Union(q[i].a,q[i].b);
}
}
for(int i=0;i<Q;i++)
if(q[i].s[0] == 'q') printf("%d\n",ans[i]);
}
return 0;
}
利用并查集判断是否是一棵树:思路是每次合并两个点,如果之前已经合并过了那么说明构成了环,不是一棵树。完成之后检查集合的个数是否为1(即图是否连通或者树的根节点是否是一个如果根节点>1那么也不是一棵树) POJ-1308
#include<iostream>
#include<set>
using namespace std;
const int maxn=105;
int pre[maxn];
set<int>s;
void init(){
for(int i=0;i<105;i++){
pre[i]=i;
}
}
int find(int x){
if(x!=pre[x]){
pre[x]=find(pre[x]);
}
return pre[x];
}
void unite(int x,int y){
x=find(x);
y=find(y);
if(x==y) return;
else pre[x]=y;
}
int main(){
int x,y;int t=0;
while(cin>>x>>y){
t++;
if(x==0&&y==0){
cout<<"Case "<<t<<" is a tree."<<endl;
continue;
}
else if(x==-1&&y==-1) break;
init();
bool check=1;
if(x==y) check=0;
else unite(x,y);
s.insert(x);
s.insert(y);
while(cin>>x>>y){
if(x==0&&y==0){
break;
}
if(find(x)==find(y)) check=0;
unite(x,y);
s.insert(x);
s.insert(y);
}
set<int>::iterator it;
set<int>st;
for(it=s.begin();it!=s.end();it++){
int now=*it;
now=find(now);
st.insert(now);
if(st.size()>1){
check=0;
break;
}
}
if(check){
cout<<"Case "<<t<<" is a tree."<<endl;
}
else{
cout<<"Case "<<t<<" is not a tree."<<endl;
}
}
}