Icebound

icebound-area

【二刷POJ】并查集总结

并查集是一种高效的数据结构,主要用来维护不同对象之间的关系。比如联通块,种类等等。
除了一般的并查集之外,还有种类并查集,关系并查集,带权并查集等等。
如果按秩合并(把小的并到大的身上),并查集的复杂度是一个奇怪的函数,查找操作接近O(1),如果不按秩合并,复杂度大概是logN,还是很高效的。
但是似乎并查集本身并不能出很多题,所以这种数据结构经常和其它算法共同使用。

最基本的并查集
POJ 1611
题意:有n个人,他们组成了k个小组,小组之间的人接触比较密切。现在0号得了传染病,而只要是得了病的人都有可能传给各自所在小组内的人,问现在有可能多少人被传染了。
说了半天其实是个联通块问题,问有多少节点与0联通。然后我就懒得写了。。。
下面是我今年6月参加自主招生前复习时写的:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int N=50000;
int f[N],n,m,ans;
int get(int x)
{
	if(f[x]!=x)f[x]=get(f[x]);
	return f[x];
}
void init()
{for(int i=1;i<=n;i++)f[i]=i;ans=0;}
int main()
{
	while(1)
	{
		scanf("%d%d",&n,&m);
		if(n==0&&m==0)break;
		init();
		for(int i=1;i<=m;i++)
		{
			int x;scanf("%d",&x);int p,q;
			scanf("%d",&q);q++;
			for(int j=2;j<=x;j++)
			{
				scanf("%d",&p);p++;
				f[get(q)]=get(p);q=p;
			}
		}
		for(int i=1;i<=n;i++)
		if(get(i)==get(1))ans++;
		printf("%d\n",ans);
	}
}

然后我还有一份我2015年寒假,也就是刚刚开始学oi时候写的代码。
从这两份代码中能看出代码风格的变化233333

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
int n,k,w,ans=0;
int s[30005];
int m[30005];
int getp(int x){if(x!=s[x])s[x]=getp(s[x]);return s[x];}
void uset(int x,int y)
{
	if((x=getp(x))==(y=getp(y)))return;
	s[x]=y;
}
int main()
{
	while(1)
	{
		scanf("%d%d",&n,&k);
		for(int i=0;i<=n;i++)s[i]=i,m[i]=0;
		if(n==0&&k==0)break;
		for(int i=0;i<k;i++)
		{
			scanf("%d",&w);
			for(int j=0;j<w;j++)
			{
				scanf("%d",&m[j]);
			}
			for(int j=1;j<w;j++)
			{
				uset(m[j-1],m[j]);
			}
		}
		for(int i=0;i<=n;i++)
		{
			if(getp(i)==getp(0))ans++;
		}
		cout<<ans<<endl;
		ans=0;
	}
    return 0;
}

很有意思,可惜我在2015年1月并不知道后面会滚粗的那么惨。
从这个水题我们总结一下并查集的两个关键操作:

第一,并
并查集本质上是个森林,合并集合操作即为把森林中的两棵树合并起来。
合并时我们其实只需要对根节点操作即可,然后随便选取一个新的根节点。
将A与B合并,其中A,B均为根节点,我们选取B为新的根节点,写成代码就是:

fa[A]=B;

这里其实以谁为根节点都无所谓,因为没有上下级关系。但是这个根结点的选取在带权并查集中有一些影响。

第二,查
查询时有一个重要的事是路径压缩,用递归实现。

int get(int x)
{
	if(x==fa[x])return fa[x];
	fa[x]=find(fa[x]);
	return fa[x];
}

递归到底,然后将下面的一点一点指向根节点。
记住,在递归到底之前做什么操作都是不对的!带权并查集血的教训!

扩展域并查集
poj 1182 食物链
经典中文题,题干略。
考虑每种生物和别的生物的关系,有同类,吃,或者被吃三种关系,所以我们建立三个域,同类域,吃域,被吃域。
然后考虑合并两种生物,以及产生的矛盾。
如果两种生物是同类,那么只需要把对应域合起来就行了。
最简矛盾情况:A的同类域是B的吃域或被吃域。
如果A吃B,那么只要把A的同类域并到B的被吃域,吃域并到B的同类域,被吃域并到B的吃域。
最简矛盾情况:A的同类域是B的同类域,A的同类域是B的吃域。
然后顺序做就行了

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=50005;
int fa[N*3],n,cnt;//同 吃  被
int get(int x)
{
	if(x!=fa[x])fa[x]=get(fa[x]);
	return fa[x];
}
int uni(int a,int b)
{
	int at=get(a),ac=get(n+a),ab=get(2*n+a);
	int bt=get(b),bc=get(n+b),bb=get(2*n+b);
	if(at==bc||at==bb)return 1;
	fa[at]=bt,fa[ac]=bc,fa[ab]=bb;
	return 0;
}
int eat(int a,int b)//a eat b
{
	int at=get(a),ac=get(n+a),ab=get(2*n+a);
	int bt=get(b),bc=get(n+b),bb=get(2*n+b);
	if(at==bt||at==bc)return 1;
	fa[at]=bb,fa[ac]=bt,fa[ab]=bc;
	return 0;
}
int main()
{
	scanf("%d%d",&n,&cnt);
	int ans=0;
	for(int i=1;i<=3*n;i++)fa[i]=i;
	while(cnt--)
	{
		int d,x,y;
		scanf("%d%d%d",&d,&x,&y);
		if(x>n||y>n){ans++;continue;}
		if(d==1)ans+=uni(x,y);
		else ans+=eat(x,y);
	}
	printf("%d\n",ans);
}

一道类似的题:poj 2912
一群人玩石头剪刀布,每个人只会出固定的石头,剪刀或者布。但是有个人是裁判,他可以随便出。给出他们之间猜拳的胜败关系,要求找出裁判,并且输出最少找多少行。如果可能有多个裁判,输出”不可确定”,如果是不合法情况,输出”矛盾”。
数据范围很小,枚举裁判,删去当前枚举的人,用食物链的方式检验是否出现矛盾,如果没矛盾,枚举的这个人就有可能是裁判。如果不成立,记录出错的行数,所有出错行数取max为找到裁判的最少行数(因为至少要找到这一行才能排除其它所有人是裁判的可能性)。如果所有的都是矛盾,则输出”矛盾”,如果有两个及以上裁判,输出”不可确定”。
这里面吃域,同类域,被吃域变成了赢域,平域,输域。
注意细节啊!

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=505,M=2005;
int fa[N*3+10],m,n;
int p1[M],p2[M],p[M];
int get(int x)
{
	if(x==fa[x]){return fa[x];}
	fa[x]=get(fa[x]);
}
void uni(int a,int b)
{
	int x=get(a),y=get(b);
	if(x==y)return;
	fa[x]=y;
}
int work(int del)
{
	for(int i=1;i<=3*n+2;i++)fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		if(p1[i]==del||p2[i]==del)continue;
		int a=p1[i],b=p2[i];
	//	cout<<"d "<<del<<" "<<a<<" "<<b<<endl;
		if(p[i]==1)
		{
			int a1=get(a),a2=get(a+n),a3=get(a+2*n);
			int b1=get(b),b2=get(b+n),b3=get(b+2*n);
		//	cout<<a1<<" "<<a2<<" "<<a3<<" "<<b1<<" "<<b2<<" "<<b3<<endl;
			if(a1==b2||a1==b3||a2==b1||a2==b3||a3==b1||a3==b2)return i;
			uni(a1,b1),uni(a2,b2),uni(a3,b3);
		}
		else
		{
			if(p[i]==3)swap(a,b);
			int a1=get(a),a2=get(a+n),a3=get(a+2*n);//同类 win lose 
			int b1=get(b),b2=get(b+n),b3=get(b+2*n);
		//	cout<<a1<<" "<<a2<<" "<<a3<<" "<<b1<<" "<<b2<<" "<<b3<<endl;
			if(a1==b1||a1==b2||a2==b2||a2==b3||a3==b1||a3==b3)return i;
			uni(a1,b3),uni(a2,b1),uni(a3,b2);
		}
	}
	return -1;
}
inline void init()
{for(int i=1;i<=n*3;i++)fa[i]=i;}
void debug()
{
	for(int i=1;i<=m;i++)
	cout<<p1[i]<<" "<<p2[i]<<" "<<p[i]<<endl;
}
int main()
{
	while(scanf("%d%d",&n,&m)!=EOF)
	{
		if(m==0)
		{
			if(n==1)puts("Player 0 can be determined to be the judge after 0 lines");
			else puts("Can not determine");
			continue;
		}
		memset(p1,0,sizeof p1);
		memset(p2,0,sizeof p2);
		memset(p,0,sizeof p);
		for(int i=1;i<=m;i++)
		{
			char s[20];
			scanf("%s",s);
			int len=strlen(s);int q=0,now=1;
			for(int j=0;j<len;j++)
			{
				if(s[j]=='='||s[j]=='<'||s[j]=='>')
				{
					q=j;
					if(s[j]=='=')p[i]=1;
					else if(s[j]=='>')p[i]=2;
					else p[i]=3;
				}
			}
			for(int j=q-1;j>=0;j--)
			{
				p1[i]+=(s[j]-'0')*now;
				now*=10;
			}
			now=1;
			for(int j=len-1;j>q;j--)
			{
				p2[i]+=(s[j]-'0')*now;
				now*=10;
			}
			p1[i]++,p2[i]++;
		}
	//	debug();
		int ans=0,flag=0;
		for(int i=1;i<=n;i++)
		{
			int t=work(i);
		//	cout<<t<<endl;
			if(t<0)
			{
				if(flag>0)//多解
				{puts("Can not determine");flag=-1;break;} 
				flag=i;
			}
			else ans=max(ans,t);
		}
		if(flag==-1)continue;
		else if(flag==0){puts("Impossible");continue;}
		else
		printf("Player %d can be determined to be the judge after %d lines\n",--flag,ans);
	}
}

另一种扩展域并查集(关系并查集)
poj1703
有两个帮派,现在给出两种操作,A a b:告知你ab为相同帮派,D a b查询ab是否属于同一帮派。
我们可以用一般的扩展域做,设两个域:同类域和不同域,合并的时候,如果是同类,那就相应的域合并,否则就反着合并,查询时如果同类域指向不同域,就是不同帮派。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
int fa[100005*2+5];
int find(int x){if(x!=fa[x])fa[x]=find(fa[x]);return fa[x];}
void add(int x,int y)
{
	if((x=find(x))==(y=find(y)))return;
	fa[x]=y;//把x并到y 
}
int n,t,q;
int main()
{
	scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&q);
		for(int i=0;i<=n+100005;i++)fa[i]=i;
		while(q--)
		{
			char s[5];
			int a,b;
			scanf("%s%d%d",s,&a,&b);
			if(s[0]=='D')
			{
				add(a,b+100005);
				add(a+100005,b);
			}
			else
			{
				if(find(a)==find(b))printf("In the same gang.\n");
				else if(find(a)==find(b+100005)||find(a+100005)==find(b))printf("In different gangs.\n");
				else
				{printf("Not sure yet.\n");}
			}
		}
	}
    return 0;
}

也可以用另一种方法,关系并查集:

#include<iostream>
#include<cstring>
#include<cstdio> 
#include<algorithm>
using namespace std;
const int N=100015;
int fa[N],s[N],n,m;
int get(int x)
{
	if(x==fa[x])return x;
	int t=fa[x];
	fa[x]=get(fa[x]);
	s[x]^=s[t];
	return fa[x];
}
void uni(int a,int b,int t)
{
	int x=get(a),y=get(b);
	if(x==y)return;
	//不需要if(x==y)
	fa[y]=x;
	s[y]=s[a]^s[b]^t;
}
int query(int a,int b)
{
	int x=get(a),y=get(b);
	if(x!=y)return -1;
	//0 相同 1 不同
	return s[a]^s[b];
}
int main()
{
	int cnt=0;
	scanf("%d",&cnt);
	while(cnt--)
	{
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++)fa[i]=i,s[i]=0;
		for(int i=1;i<=m;i++)
		{
			char ss[10];int a,b;
			scanf("%s%d%d",ss,&a,&b);
			if(ss[0]=='D')uni(a,b,1);
			else
			{
				if(n==2)
				{
					if(a!=b)
					puts("In different gangs.");
					else puts("In the same gang.");
					continue;
				}//特判 
				int t=query(a,b);
				if(t<0)puts("Not sure yet.");
				else puts(t?"In different gangs.":"In the same gang.");
			}
		}
	}
	return 0;
}

s[x]表示自己与根结点的关系,s[x]=0代表同类,s[x]=1代表异类。
s[x]初始值为s[x]=0 自己与自己为同类
路径压缩时,s[x]=s[x]^s[fa[x]] 其中,fa[x]是s[x]直系亲属,而不是路径压缩后那个根,而且,注意在递归结束返回时进行这个操作!具体为啥,自己想!!!
合并时,s[new_root]=s[a]^s[b]^t,如果同类,t=0,异类,t=1。
这个没有扩展域好写。。但是写了之后莫名喜欢。。。。
poj1733
有一个长度已知的01串,给出[l,r]这个区间中的1是奇数个还是偶数个,给出一系列语句问前几个是正确的。
把[l,r]区间中的1的个数表示为sum[r]-sum[l-1],如果sum[r]-sum[l-1]是偶数,说明sum[r]和sum[l-1]的奇偶性相同,否则奇偶性不同,这样又变成了是否相同集合的问题,可以用扩展域或者关系并查集来做。注意离散化!

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=20000;
int fa[N],d[N],m,n,ans;
int seq[N],cnt,ll[N],rr[N];
int opt[N];
int get(int x)
{
	if(fa[x]==x)return x;
	int t=fa[x];
	fa[x]=get(fa[x]);
	d[x]=d[x]^d[t];
	return fa[x];
}
bool add(int a,int b,int c)
{
	int x=get(a),y=get(b);
	if(x==y)
	{
		if(d[a]==d[b]&&c==1)//1 为不同 
		return 0;
		if(d[a]!=d[b]&&c==0)//0 为相同
		return 0;
		return 1;
	}
	fa[y]=x;
	d[y]=d[a]^d[b]^c;
	return 1;
}
int main()
{
	while(scanf("%d%d",&m,&n)!=EOF)
	{
		cnt=ans=0;
		for(int i=1;i<=n;i++)
		{
			int l,r;char s[10];
			scanf("%d%d%s",&l,&r,s);
			l--;
			seq[++cnt]=l,seq[++cnt]=r;
			ll[i]=l,rr[i]=r;
			opt[i]=(s[0]=='o');
		}
		sort(seq+1,seq+cnt+1);
		for(int i=0;i<=cnt+5;i++)fa[i]=i,d[i]=0;
		for(int i=1;i<=n;i++)
		{
			int l,r;
			l=lower_bound(seq+1,seq+cnt+1,ll[i])-seq;
			r=lower_bound(seq+1,seq+cnt+1,rr[i])-seq;
			if(!add(l,r,opt[i]))break;
			ans++;
		}
		printf("%d\n",ans);
	}
	return 0;
}

poj 1417
给出p1+p2个人,其中p1个是好人,p2个是坏人。然后有一些关系 ,a说b是好人(坏人).其中没有矛盾的,判断是否有唯一解判断哪些人是好人,哪些人是坏人。
如果一个人说另一个人是好人,如果这个人是好人,那么另一个人是好人,如果这个人是坏人,那么另一个人是坏人。
如果一个人说另一个人是坏人,如果这个人是坏人,那么另一个人是好人,如果这个人是好人,那么另一个人是坏人。
可得 如果一个人说另一个是好人,那么这两个人属于同一集合,否则属于不同集合。
用并查集处理后,组成k棵树,对于每棵树处理,把好人和坏人挑出来,分别放在q[i][0]和q[i][1]中
i个集合,选了j个好人/坏人 (我们不知道选的是好人或者坏人,只有相对的两个集合)
f[i][j]=f[i-1][j-q[i][0]]+f[i-1][j-q[i][1]] (选好人或者坏人)
只需要看f[k][p1]或者f[k][p2]是否小于2就行,输出方案有点恶心。。。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1005;
int fa[N];int r[N];
int n,cnt,path[N][N];
int get(int x)
{
	if(x==fa[x])return x;	
	int t=fa[x];
	fa[x]=get(fa[x]);
	r[x]^=r[t];
	return fa[x];
}
void uni(int a,int b,int x)
{
	int aa=get(a);
	int bb=get(b);
	if(aa==bb)return;
	fa[bb]=aa;
	r[bb]=r[a]^r[b]^x;
}
int ans[N][3];
int q[N][3],a[N];//集合人数,集合编号
int f[N][N];//i个集合,选了j个好人
//f[i][j]=f[i-1][j-q[i][0]]+f[i-1][j-q[i][1]]
//意思是假设这组人是好人或坏人
void init()
{
	for(int i=1;i<=n;i++) 
	if(get(i)==i)a[i]=++cnt;//新的一组集合
	for(int i=1;i<=n;i++)
	{
		q[a[get(i)]][r[i]]++;
	}
}
void dp()
{
	f[0][0]=1;//0组集合 0人 方案数为1 
	for(int i=1;i<=cnt;i++)
	for(int j=0;j<=n;j++)
	{
		if(j-q[i][0]>=0&&f[i-1][j-q[i][0]]>0)
		{
			f[i][j]+=f[i-1][j-q[i][0]];
			path[i][j]=q[i][0];
		}
		if(j-q[i][1]>=0&&f[i-1][j-q[i][1]]>0)
		{
			f[i][j]+=f[i-1][j-q[i][1]];
			path[i][j]=q[i][1];
		}
		if(f[i][j]>1)f[i][j]=2;
	}
}
int main()
{
	int t,p1,p2;
	while(scanf("%d%d%d",&t,&p1,&p2)!=EOF)
	{
		if(t==0&&p1==0&&p2==0)return 0;
		for(int i=1;i<=p1+p2;i++)fa[i]=i;
		memset(r,0,sizeof r);
		memset(f,0,sizeof f);
		memset(path,0,sizeof path);
		memset(q,0,sizeof q);
		memset(a,0,sizeof a);
		memset(r,0,sizeof r);
		memset(ans,0,sizeof ans);
		cnt=0;
		n=p1+p2;
		for(int i=1;i<=t;i++)
		{
			char c[10];int a,b;
			scanf("%d%d%s",&a,&b,c);
			uni(a,b,c[0]=='n');
		}
		init();
		dp();
		if(f[cnt][p1]!=1)puts("no");
		else
		{
			for(int i=cnt,j=p1;i>0&&j>0;i--)
			{
				if(q[i][0]==path[i][j])ans[i][0]=1;
				else ans[i][1]=1;
				j-=path[i][j];
			}
			for(int i=1;i<=n;i++) 
		    if(ans[a[get(i)]][r[i]])printf("%d\n",i);
		    printf("end\n");
		}
	}
}

带权并查集
把每个节点加上一个到根结点的距离,就可以叫带权并查集了。在路径压缩的时候记着把路径都加上。合并的时候要具体情况具体分析。
poj1984
给出一个地图(只有四个方向上的距离),询问任意两点间曼哈顿距离。
带权并查集记录上和右的距离,合并时合并根节点,并直接算出到旧根到新根的距离,更新权值。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=100000;
int fa[N],rd[N],dd[N];//right dist down dist
int a[N],b[N],r[N],d[N],tot,n,m;
void add(int aa,int bb,int cc,int dd)
{
	tot++;
	a[tot]=aa,b[tot]=bb,r[tot]=cc,d[tot]=dd;
}
struct seq
{
	int a,b,c,ans,k;
}q[10005];
bool cmp(seq t1,seq t2)
{return t1.c<t2.c;}
bool cmp2(seq t1,seq t2)
{return t1.k<t2.k;}
int get(int x)
{
	if(x==fa[x])return x;
	int t=fa[x];
	fa[x]=get(fa[x]);
//	cout<<"get "<<rd[t]<<" "<<dd[t]<<endl;
	rd[x]+=rd[t];
	dd[x]+=dd[t];
	return fa[x];
}
void uni(int a,int b,int r,int d)
{
	//cout<<"uni "<<a<<" "<<b<<" "<<r<<" "<<d<<endl; 
	int x=get(a),y=get(b);
	if(x==y)return;
	fa[y]=x;
	rd[y]=rd[a]-rd[b]+r;
	dd[y]=dd[a]-dd[b]+d;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		fa[i]=i;
	}
	for(int i=1;i<=m;i++)
	{
		char s[10];
		int aa,bb,cc;
		scanf("%d%d%d%s",&aa,&bb,&cc,s);
		// 'N''E''S' 'W'
		if(s[0]=='N')//a向北走c到b 
		add(aa,bb,0,-cc);
		else if(s[0]=='E')//a向东走c到b 
		add(aa,bb,cc,0);
		else if(s[0]=='S')
		add(aa,bb,0,cc);
		else//west 西 
		add(aa,bb,-cc,0);
	} 
	int cnt=0;
	scanf("%d",&cnt);
	for(int i=1;i<=cnt;i++)
	scanf("%d%d%d",&q[i].a,&q[i].b,&q[i].c),q[i].k=i;
	sort(q+1,q+cnt+1,cmp);
	int now=0;
	for(int i=1;i<=cnt;i++)
	{
	//	cout<<now<<endl;
		if(now<q[i].c)
		{
			for(int j=now+1;j<=q[i].c;j++)
			uni(a[j],b[j],r[j],d[j]),now=j;
		}
	//	cout<<q[i].a<<" "<<q[i].b<<endl;
		int x=get(q[i].a),y=get(q[i].b);
	//	cout<<x<<" tt "<<y<<" "<<q[i].a<<" "<<q[i].b<<endl;
	//	cout<<x<<" x "<<y<<endl;
		if(x!=y)q[i].ans=-1;
		else
		{
		//	cout<<rd[q[i].a]<<" "<<rd[q[i].b]<<" "<<dd[q[i].a]<<" "<<dd[q[i].b]<<endl;
			q[i].ans=abs(rd[q[i].a]-rd[q[i].b])+abs(dd[q[i].a]-dd[q[i].b]);
		}
	}
	sort(q+1,q+1+cnt,cmp2);
	for(int i=1;i<=cnt;i++)printf("%d\n",q[i].ans);
	return 0;
}

全文完。。。