数据结构---并查集小结
来源:互联网 发布:linux grub2 修复 编辑:程序博客网 时间:2024/06/06 19:42
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
大体分为三个:普通的并查集,带种类的并查集,扩展的并查集(主要是必须指定合并时的父子关系,或者统计一些数据,比如此集合内的元素数目。)
1 #define MAXN 100005 2 int n,m,k,fa[MAXN]; 3 int rank[MAXN]; 4 void init(int n)//初始化 5 { 6 for(int i=0;i<=n;i++) 7 { 8 fa[i]=i; 9 rank[i]=0;10 }11 }12 //查找的时候,进行路径压缩fa[x]=find(fa[x])13 //把查找路径上的结点都指向根结点,减少树的高度。14 int find(int x)15 {16 if(x != fa[x])17 fa[x]=find(fa[x]);//路径压缩18 return fa[x];19 }20 //合并21 void unio(int x,int y)22 {23 int fx=find(x),fy=find(y);24 if(fx==fy) return ;25 if(rank[fy]<rank[fx])//将rank值小的合并到大的中26 fa[fy]=fx;27 else28 {29 fa[fx]=fy;30 if(rank[fx]==rank[fy])31 rank[fy]++;32 }33 }34 //或(忽略按秩合并,懒的时候经常这么敲.....时间上也不知道会差多少,没有试过。。):35 void unio(int x,int y)36 {37 int fx=find(x),fy=find(y);38 if(fx==fy) return ;39 fa[fy]=fx;40 }
一.普通并查集:
Poj 1611 ,2524,2236.都是裸的并查集。
简单并查集的一个应用:kruskal需要并查集判断点是否在同一个集合里。
Poj1287
模版最小生成树:
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 using namespace std; 6 #define MAXN 55 7 #define MAXM 10000 8 int fa[MAXN]; 9 int n,m,e,ans;10 struct Edge11 {12 int u;13 int v;14 int c;15 }p[MAXM];16 void addEdge(int u,int v,int c)17 {18 p[e].v=v;p[e].c=c;p[e].u=u;19 e++;20 }21 void init()22 {23 for(int i=0;i<=n;i++)24 fa[i]=i;25 }26 int find(int x)//查找点所在的集合27 {28 if(fa[x]!=x)29 fa[x]=find(fa[x]);30 return fa[x];31 }32 int cmp(const Edge &a,const Edge & b)33 {34 return a.c<b.c;35 }36 bool kru(int n,int m)37 {38 int i,j;39 sort(p,p+m,cmp);40 ans=0;41 init();42 int cnt=0;43 for(i=0;i<m;i++)44 {45 //使用并查集的地方,在每次加入边之前先判断下点是否已经在同 //一个集合了46 int uu=find(p[i].u);47 int vv=find(p[i].v);48 if(uu==vv)49 continue;50 fa[uu]=vv;51 ans+=p[i].c;52 cnt++;53 }54 if(cnt != n-1)55 return false;56 else57 return true;58 }59 int main()60 {61 while(scanf("%d",&n))62 {63 e=0;64 if(!n)65 break;66 scanf("%d",&m);67 for(int i=0;i<m;i++)68 {69 int a,b,c;70 scanf("%d%d%d",&a,&b,&c);71 addEdge(a,b,c);72 }73 kru(n,m);74 printf("%d\n",ans);75 }76 return 0;77 }
二.种类并查集:
最经典的就是 POJ 1182 食物链
题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)
在做这题之前就知道是很经典的并查集了,还是不会做。。。,看了网上很多份解题报告,花了很长的时间来理解这题,下面这份报告的思路http://cavenkaka.iteye.com/blog/1489588 讲的很不错。下面是我根据从网上的解题报告中整理总结的:
思路:
fa[x]表示x的根结点。relat[x]表示fa[x]与x的关系。relat[x] == 0 表示fa[x]与x同类;1表示fa[x]吃x;2表示x吃fa[x]。{relat[]可以抽象成元素i到它的父亲节点的逻辑距离,见下面。}
怎样判断一句话是不是假话?
假设已读入 D , X , Y , 先利用find()函数得到X , Y 所在集合的代表元素 fx,fy ,若它们在同一集合(即 fx== fy )则可以判断这句话的真伪:
1.若 D == 1 (X与Y同类)而 relat[X] != relat[Y] 则此话为假。(D == 1 表示X与Y为同类,而从relat[X] != relat[Y]可以推出 X 与 Y 不同类。比如relat[x]=0 即fx与x同类,而relat[y]=1 即fy吃y,而fx==fy,故矛盾。)
2.若 D == 2 (X吃Y)而 relat[X] == relat[Y] (X 与Y为同类,故矛盾。)或者 relat[X] == ( relat[Y] + 1 ) % 3 (Y吃X )则此话为假。
上个问题中 r[X] == ( r[Y] + 1 ) % 3这个式子怎样推来?
我们来列举一下: 假设有Y吃X(注意fx==fy的前提条件),那么r[X]和r[Y]的值是怎样的?
r[X] = 0 && r[Y] = 2 (X与fx同类,Y吃fy,即Y吃X)
r[X] = 1 && r[Y] = 0 (X被fx吃,Y与fy同类,即Y吃X)
r[X] = 2 && r[Y] = 1 (X吃fx,Y被fy吃,一个环,Y吃X)
通过观察得到r[X] = ( r[Y] + 1 ) % 3;
对于上个问题有更一般的判断方法(来自poj 1182中的Discuss ):
若 ( r[x] - r[y] + 3 ) % 3 != d - 1(d-1 值是1或者0.....) ,则此话为假。
当判断两个元素的关系时,若它们不在同一个集合当中,则它们还没有任何关系,直接将它们按照给出的关系合并就可以了。若它们在同一个集合中,那么它们的关系就是x到y的距离:如图所示为(r[x]+3-r[y])%3,即x与y的已有的关系表达,判断和给出的关系是否一致就可以知道是真话还是假话了。
注意事项:
A、find()函数里面的那句relat[x]=(relat[x] + relat[t])%3解释:
我们用x--r-->y表示x和y之间的关系是r,比如x--1--y代表x吃y。现在,若已知x--r1-->y,y--r2-->z,如何求x--?-->z?
即如何在路径压缩的时候更新x与当前父亲的relat值?
X--r[x]--t(t就是还未压缩的父亲),t---r[t]---root(压缩后的父亲)。
故x---r[x]+r[t]--->root;举例:
r[x]=0;r[t]=1;则x与root的关系是x被root吃。。。。其他类似。
用逻辑距离理解如下(向量思想):
B、当D X Y时,则应合并X的根节点和Y的根节点,同时修改各自的relat。那么问题来了,合并了之后,被合并的根节点的relat值如何变化呢?
现有x和y,d为x和y的关系,fx和fy分别是x和y的根节点,于是我们有x--relat[x]-->fx,y--relat[y]-->fy,显然我们可以得到fx--(3-relat[x])-->x,fy--(3-relat[y])-->y。假如合并后fx为新的树的根节点,那么原先fx树上的节点不需变化,fy树则需改变了,因为relat值为该节点和树根的关系。这里只改变relat(fx)即可,因为在进行find操作时可相应改变fy树的所有节点的relat值。于是问题变成了fx--?-->fy。我们不难发现fx--(3-relat[x])-->x--d-->y--relat[y]-->fy,我们有fx--(3-relat[x])-->x--d-->y--relat[y]-->fy。我们求解了fx和fy的关系。即fx----(relat[y] - relat[x] +3 +d)%3--->fy。(如下图:)
1 //食物链 2 //!!!!!!! 3 #include <iostream> 4 #include <cstdio> 5 #include <cstring> 6 7 using namespace std; 8 9 #define MAXN 5001010 int N,M,K,fa[MAXN],relat[MAXN];//ralat 表示与父亲的关系,0表示是同类,1表示是x被fa[x]吃,2表示是吃父亲11 int ans=0;12 void init(int n)13 {14 for(int i=0;i<=n;i++)15 {16 fa[i]=i;17 relat[i]=0;18 }19 20 }21 22 int find(int x)23 {24 if( x != fa[x])25 {26 int t=fa[x];27 fa[x]=find(fa[x]);28 relat[x]=(relat[x] + relat[t])%3;//A29 }30 return fa[x];31 }32 33 void unio(int x,int y,int d)//d是x,y的关系34 {35 int fx=find(x);36 int fy=find(y);37 fa[fx]=fy;38 relat[fx]=(relat[y] - relat[x] +3 +d)%3;//B39 }40 41 int main()42 {43 int d,x,y;44 scanf("%d%d",&N,&K);45 ans=0;46 init(N);47 while(K--)48 {49 scanf("%d%d%d",&d,&x,&y);50 if(x>N || y>N)51 {52 ans++;53 continue;54 }55 if(d==2 && x==y)56 {57 ans++;58 continue;59 }60 int fx=find(x);61 int fy=find(y);62 if(fx==fy)63 {64 if((relat[x] - relat[y] +3)%3 != d-1)//65 ans++;66 }67 else68 {69 unio(x,y,d-1);//d-1==1表示的是x与y的关系70 }71 }72 printf("%d\n",ans);73 return 0;74 }
POJ上的种类并查集还有:
POJ-1703、POJ-2492、POJ-1733、POJ-1988等。
POJ-1703 Find them, Catch them(两个互斥集合)
题目大意是:有两个帮派,告诉你那两个人属于不同的帮派,让你判断某两个人得是否在一个帮派中。
并查集的核心是用集合里的一个元素代表整个集合,集合里所有元素都指向这个元素,称它为根元素。集合里任何一个元素都能到达根元素。这一题里,设数组fa[x]表示x的父亲是fa[x](x,fa[x]在一个帮派),diff[x]表示x与diff[x]不在同一个集合里面。
如果是D[b][c]命令的话,即b与c不在同一个帮派,故b与diff[c]在同一个帮派。把b放到diff[c]的集合里,同理把c放到diff[b]里面。
1 if(diff[b] == -1)2 diff[b]=c;3 if(diff[c] == -1)4 diff[c]=b;5 unio(b,diff[c]);6 unio(c,diff[b]);
如果是A命令的话,查询b,c的根元素:
1. 根元素相同,b,c在同一个集合里;
2. 根元素不同,但b与diff[c]的根元素相同,b,c不在一个集合里;
3.否则,b,c还没有确定。
POJ-2492与1703基本一样。
另解(更一般的解):这两题可以用食物链的形式写,即简化的食物链,比食物链少一个关系,即相当于1吃2,2吃1。
对应关系即为:
x--(r1+r2)%2->z
fx----(relat[y] - relat[x] +2+d)%2--->fy
下面给出1703的食物链改编版:
1 //食物链改编版 2 //!!!!!!! 3 #include <iostream> 4 #include <cstdio> 5 #include <cstring> 6 using namespace std; 7 #define MAXN 100010 8 int N,M,K,fa[MAXN],relat[MAXN];//此题中0表示在同一类,1表示不在同一类。 9 int ans=0;10 void init(int n)11 {12 for(int i=0;i<=n;i++)13 {14 fa[i]=i;15 relat[i]=0;16 }17 }18 int find(int x)19 {20 if( x != fa[x])21 {22 int t=fa[x];23 fa[x]=find(fa[x]);24 relat[x]=(relat[x] + relat[t])%2;//A25 }26 return fa[x];27 }28 void unio(int x,int y,int d)//d是x,y的关系29 {30 int fx=find(x);31 int fy=find(y);32 fa[fx]=fy;33 relat[fx]=(relat[y] - relat[x] +2 +d)%2;//B34 }35 int main()36 {37 int x,y;38 char op;39 char buf[10];40 int t;41 scanf("%d",&t);42 while(t--)43 {44 scanf("%d%d",&N,&M);45 ans=0;46 init(N);47 while(M--)48 {49 getchar();50 scanf("%s%d%d",&buf,&x,&y);//用cin tle。。。。51 op=buf[0];52 if(op=='D')53 {54 unio(x,y,1);//1代表着x,y不在同一类,即食物链中的x吃y。55 }56 else57 {58 int fx=find(x),fy=find(y);59 if(fx==fy)60 {61 if((relat[x] - relat[y] +2)%2 ==1)//用食物链的观点来看,即x,y不在同一类。62 {63 printf("In different gangs.\n");64 }65 else66 printf("In the same gang.\n");67 }68 else69 {70 printf("Not sure yet.\n");71 }72 }73 }74 }75 return 0;76 }
POJ-1733 Parity game
题目大意:这题的大意是对于一个正整数的区间,有若干句话,判断第一句错误的位置,每句话所描述的意思是对于一个区间[a, b]有奇数个1或是偶数个1。
思路:
设s[i]表示前i个数中1的个数,则s[0]=0;则信息i j even等价于s[j]-s[i-1]为偶数,即s[j]与s[i-1]同奇偶。这样,每条信息都可以变为s[i-1]和s[j]是否同奇偶的信息。
若记:
fa[j]为当前和fa[j]同奇偶的元素集合,
diff[j]为和fa[j]不同奇偶的元素集合,
则一条信息i j even表明i-1,j有相同的奇偶性,将导致fa[j]和fa[i-1]合并,diff[j]和diff[i-1]合并;
i j odd表明i-1,j的奇偶性不同,将导致fa[j]和diff[i-1]合并(fa[j]与fa[i-1]奇偶性不同即与diff[i-1]奇偶性相同);diff[j]和fa[i-1]合并。
最后这题还必须得离散化,因为原来的区间太大,可以直接HASH一下,离散化并不会影响最终的结果,
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 using namespace std; 5 #define MAXN 10010 6 #define HASH 9941 7 int N,K; 8 int fa[MAXN],diff[MAXN],rank[MAXN]; 9 int hash[MAXN];10 void init()11 {12 for(int i=0;i<MAXN;i++)13 {14 hash[i]=-1;15 diff[i]=-1;16 fa[i]=i;17 rank[i]=0;18 }19 }20 int find(int x)21 {22 if(x==-1) return -1;23 if(x == fa[x]) return x;24 fa[x]=find(fa[x]);25 return fa[x];26 }27 void unio(int x,int y)28 {29 if(x==-1 || y==-1) return;30 int fx=find(x),fy=find(y);31 if(fx==fy) return ;32 if(rank[fx]>rank[fy])33 fa[fy]=fx;34 else35 {36 fa[fx]=fy;37 if(rank[fx]==rank[fy])38 rank[fy]++;39 }40 }41 int main()42 {43 scanf("%d%d",&N,&K);44 init();45 int a,b,sa,sb,da,db,ha,hb;46 char s[10];47 for(int i=1;i<=K;i++)48 {49 scanf("%d%d%s",&a,&b,&s);50 a--;51 ha=a%HASH;52 while(hash[ha] != -1 && hash[ha] !=a)53 ha = (ha+1) %HASH;54 hash[ha] = a;55 a=ha;56 hb = b % HASH;57 while(hash[hb] != -1 && hash[hb] != b)58 hb =(hb+1) %HASH;59 hash[hb] =b;60 b=hb;61 //将a,b,diff[a],diff[b]的根结点找出来,再按要求合并62 sa=find(a);63 da=find(diff[a]);64 sb=find(b);65 db=find(diff[b]);66 if(s[0]=='e')67 {68 if(sa== db || da==sb)69 {70 printf("%d\n",i-1);71 return 0;72 }73 if(diff[a]==-1) diff[a]=db;74 if(diff[b]==-1) diff[b]=da;75 unio(sa,sb);76 unio(da,db);77 }78 else if(s[0]=='o')79 {80 if(sa ==sb || (da != -1 && da== db))81 {82 printf("%d\n",i-1);83 return 0;84 }85 if(diff[a] == -1) diff[a] = sb; 86 if(diff[b] == -1) diff[b] = sa;87 unio(sa, db); 88 unio(da, sb);89 }90 }91 printf("%d\n",K);92 return 0;93 }
POJ-1988 Cube Stacking
题意:是给出N个立方体,可以将立方体移动到其它立方体形成堆,然后有P个下面的操作: 1) M X Y ,将X立方体所在的堆移到Y立方体所在的堆的上面; 2) C X 输出在X所在的堆上,在X立方体下面的立方体个数。
思路:
用三个数组,fa,ans,sum, fa[i]表示i的根结点,ans[i]表示i的结果,即压在i下面的立方体个数,sum[i]表示i所在的堆的立方体总个数。对于每一堆立方体,根结点使用堆底的立方体,而且在这个堆所对应的集合内,通过更新,使得只有根结点的sum值为这堆的总个数,ans值为0(因为它在堆底),其它的立方体的sum值都为0,ans值在并查集的查找步骤中进行递归更新。
在并查集的查找函数的执行中,先向上找到根结点,并且保存当前结点x的父节点为tmp,找到根结点后,向下依次一更新结点的ans,sum值。
1)若sum[x]不为0,即表示x是一个堆的堆底元素,ans[x]为0,其父节点是另外一堆的堆底(因为在并查集的操作中,通过将一个堆的堆底指向另一个堆的堆底来实现合并), ans[x]+=sum[tmp],sum[tmp]+=sum[x],sum[x]=0 ,这三个语句将x的ans值加上父结点的总个数(因为是将x所在的堆放在父节点的堆,所有x下面的正方体个数加上刚刚放上去的父亲的值),然后将父节点的sum值加上x的sum值(父节点的堆的总数变为两者之和),然后再将x的sum值置0.
2)若sum[x]为0,即表示x不是堆底,那么只要将x的ans值加上父节点(此父亲是原来的父亲tmp,因为在前面的更新中此父亲已经被更新了。所有他的ans值即为压在他下面的正方体个数)的ans值即可。ans[x]+=ans[tmp]。下面是并查集的几个函数。在合并操作里面,合并完后我们再对x,y执行一次查找操作以更新对应堆的值,因为在下次合并的时候可能堆还没有来得及更新。
1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 using namespace std; 5 #define MAXN 30010 6 int fa[MAXN],ans[MAXN],sum[MAXN]; 7 int P; 8 void init() 9 {10 for(int i=0;i<MAXN;i++)11 {12 fa[i]=i;13 ans[i]=0;14 sum[i]=1;15 }16 }17 int find(int x)18 {19 int tmp;20 if(x != fa[x])21 {22 tmp=fa[x];23 fa[x]=find(fa[x]);24 if(sum[x] != 0)25 {26 ans[x] += sum[tmp];27 sum[tmp] += sum[x];28 sum[x] =0;29 }30 else31 {32 ans[x] += ans[tmp];33 }34 }35 return fa[x];36 }37 void unio(int x,int y)38 {39 int fx=find(x);40 int fy=find(y);41 fa[fx]=fy;42 }43 int main()44 {45 char c;46 int a,b;47 init();48 scanf("%d",&P);49 while(P--)50 {51 getchar();52 scanf("%c",&c);53 if(c=='M')54 {55 scanf("%d%d",&a,&b);56 unio(a,b);57 find(a);//每次合并后都得更新,防止下次合并出错58 find(b);59 }60 else61 {62 scanf("%d",&a);63 find(a);64 printf("%d\n",ans[a]);65 }66 }67 }
一点心得:
个人感觉对于那些种类并查集应该都可以用食物链的关系来理解的,通过记录与根结点的关系来判断是否在一个集合。。。刚刚把poj1703翻译成食物链版本,下次试试把上面这些题都翻译成食物链版本。。。。
一些其他的并查集题目汇总:
http://hi.baidu.com/czyuan%5Facm/blog/item/531c07afdc7d6fc57cd92ab1.html
- 数据结构---并查集小结
- 数据结构专题小结:并查集
- 并查集小结
- 并查集小结
- 并查集小结
- 并查集小结
- 并查集小结
- 并查集小结
- 并查集小结
- 并查集小结
- 并查集小结
- 数据结构-并查集
- 数据结构-并查集
- 并查集【数据结构】
- 【数据结构】并查集
- 数据结构-并查集
- 数据结构 并查集
- 数据结构 并查集
- java web 开发(二)
- 软件开发
- sql函数的使用-日期函数(11)
- objc_msgSend 报错
- CGRectGetMaxX&CGRectGetMidX的用法
- 数据结构---并查集小结
- SQL - INDEX
- 太阳能计算
- 从头开始写项目Makefile(七):统一目标输出目
- 【Python】按行读取文件、IOError: [Errno 22] invalid mode ('a+') or filename,处理文件的换行符
- 未解——Matlab solve函数
- 如何用4M网速为30个程序猿提供特殊服务
- findbugs类介绍(BetterVisitor)
- 数据库管理(12)