您的位置:首页 > 其它

POJ2524 并差集以及优化(路径压缩+按秩合并)的简洁介绍

2016-07-29 23:57 537 查看
0 题意很简单,略。

1

(下面的描述中,所提及的集合的深度和集合的宽度是指:

以1,3,4都指向2为例:
2
/  |  \
1   3   4
树的深度为2(两层),树的宽度为3(1 3 4 三个)



①关于按秩合并中秩的一点说明,我理解的秩是集合的深度,大部分博客的代码与我的理解相同(即2-④ Join()中按秩合并代码中的秩就是严格代表着集合的深度),但是有的博客上:

p1=Find(a),p2=Find(b);
if(rank[p1]<=rank[p2]){
parents[p1]=p2;
rank[p2]+=rank[p1];
}
else if(rank[p2]<=rank[p1]){
parents[p2]=p1;
rank[p1]+=rank[p2];
}



两种写法都是对的,虽然没有说明,但是这里的秩严格来说已经不代表深度,不过却同样起到了根据深度来进行合并的控制作用,不同的是多加了一个规则:在深度相同时,宽度更大的旧集合的代表优先成为新集合的代表,这一点对于路径压缩的优化作用不大。因为路径压缩的效率与集合的深度有关,与宽度无关。如果没有充分理解或许会对两种写法感到疑惑,特此说明,如有不同理解,欢迎指正。

②路径压缩和按秩合并的举例理解:

1)路径压缩(对Find()优化,只需参考2①,2②、2③中的Find()部分)

//路径压缩前,每次查找最低端的元素的最高负责人(这个集合的代表),都要不断往上查对它负责的元素,直到查到最后那个只需要对自己负责的元素即为这一连串元素的最高负责人。
1->2, Find(1)=Find(2)=2
1->2->3, Find(1)=Find(2)=Find(3)=3;
1->2->3->4, Find(1)=Find(2)=Find(3)=Find(4);

//路径压缩后,每次查找最低端的元素的代表完毕之后,都顺便将该元素以及该元素上面的所有元素的负责人都设置为直接对最高负责人负责,就方便了再次查找。
1->2, Find(1)=Find(2)=2
1->2->3 ---- 1->3,2->3, Find(1)=3,Find(2)=3,Find(3)=3
1->2->3->4 ---- 1->4,2->4,3->4, Find(1)=4,Find(2)=4,Find(3)=4,Find(4)=4;



2)按秩合并(对Join()优化,只需参考2①,2④中的Join()部分)

//按秩合并前,有可能出现一个长串,那么路径压缩时,需要改变的值就很多
Join(1,2)  1->2
Join(2,3)  1->2->3
Join(3,4)  1->2->3->4
Find(1)    1->4,2->4,3->4 //(路径压缩时需要改变三个值,即与被并入集合的深度有关)

//按秩合并后,总是尽可能使合并后的新集合的深度不会继续加深,那么路径压缩时,改变的值就相对较少
Join(1, 2)
2   3   4
/
1

Join(2, 3)
2    4
/   \
1     3

Join(3, 4)
2
/  |  \
1   3   4

Find(1)=2, 1->2 //(路径压缩时相对改变较少的值)


2关于POJ2524的AC CODE

①朴素代码

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
///朴素代码(没有路径压缩,也没有按秩合并)
using namespace std;
int n,m;
int kase=0;
int parents[50010];
int cnt;//记录最后答案,每次Join成功,就自减减一次,最后得到的即一共有多少个集合(当然也可以最后走一个for,有一个数的root是指向自己的就自加加一次,那么得到的代表个数自然就是集合个数)
int Find(int x){
int root=x;
while(root!=parents[root]){
root=parents[root];//找到x的真正根节点,即最后的root
}
return root;
}
void Join(int a,int b){
int p1=Find(a);
int p2=Find(b);
if(p1==p2){
return ;
}
parents[p1]=p2;//不按秩合并,那么新的集合的代表由原来的两个旧集合中哪个旧集合的代表来充当就不重要了,于是随便指定了一个
cnt--;
return ;
}
int main()
{
while(~scanf("%d%d",&n,&m)&&n!=0){
cnt=n;
for(int i=1;i<=n;i++){
parents[i]=i;
}
int a;
int b;
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
Join(a,b);
}

cout<<"Case "<<++kase<<": "<<cnt<<endl;
}

return 0;
}


②递归的路径压缩

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
///递归的路径压缩(对Find()的优化)
using namespace std;
int n,m;
int kase=0;
int cnt;
int parents[50010];
int Find(int x){
int root=x;
if(root!=parents[root])
root=Find(parents[root]);//找到x的真正根节点,即最后的root<span style="font-family: Arial, Helvetica, sans-serif;">,同时在递归回溯时,将中间经过的元素的直接负责人都设置为最高负责人,方便下一次查找</span>
return root;
}
void Join(int a,int b){
int p1=Find(a);
int p2=Find(b);
if(p1==p2){
return ;
}

parents[p1]=p2;//不按秩合并,那么新的集合的代表由原来的两个旧集合中哪个旧集合的代表来充当就不重要了,于是随便指定了一个
cnt--;
return ;
}
int main()
{
while(~scanf("%d%d",&n,&m)&&n!=0){
cnt=n;
for(int i=1;i<=n;i++){
parents[i]=i;
}

int a;
int b;
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
Join(a,b);
}

cout<<"Case "<<++kase<<": "<<cnt<<endl;
}

return 0;
}


③非递归路径压缩

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
///非递归(防止栈溢出)路径压缩(对Find()的优化)
using namespace std;
int n,m;
int kase=0;
int cnt;
int parents[50010];
int Find(int x){
int root=x;
while(root!=parents[root]){
root=parents[root];//找到x的真正根节点,即最后的root
}
int temp2;
while(x!=parents[x]){
temp2=parents[x];//重新寻找一遍,并将途中所有节点都指向根节点,即压缩路径,不用递归防止栈溢出
parents[x]=root;
x=temp2;
}
return root;
}
void Join(int a,int b){
int p1=Find(a);
int p2=Find(b);
if(p1==p2){
return ;
}

parents[p1]=p2;//不按秩合并,那么新的集合的代表由原来的两个旧集合中哪个旧集合的代表来充当就不重要了,于是随便指定了一个
cnt--;
return ;
}
int main()
{
while(~scanf("%d%d",&n,&m)&&n!=0){
cnt=n;
for(int i=1;i<=n;i++){
parents[i]=i;
}

int a;
int b;
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
Join(a,b);
}

cout<<"Case "<<++kase<<": "<<cnt<<endl;
}

return 0;
}


④非递归路径压缩+按秩合并

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
///非递归(防止栈溢出)路径压缩(对Find()的优化)+按秩合并(对Join()的优化)
///(先理解路径压缩然后就容易理解按轶合并的必要性,正是因为按轶(深度)合并的存在,使得合并之后尽量增加树的宽度而不增加深度(当两个集合的深度相同时合并后形成的新集合是原来的集合的深度+1),从而使路径压缩尽可能压缩的路径层数变少)
using namespace std;
int n,m;
int kase=0;
int cnt;
int parents[50010];
int rank[50010];//以此为代表的集合的深度,用于合并时的比较
int Find(int x){
int root=x;
while(root!=parents[root]){
root=parents[root];//找到x的真正根节点,即最后的root
}
int temp2;
while(x!=parents[x]){
temp2=parents[x];//重新寻找一遍,并将途中所有节点都指向根节点,即压缩路径
parents[x]=root;
x=temp2;
}
return root;
}
void Join(int a,int b){
int p1=Find(a);
int p2=Find(b);
if(p1==p2){
return ;
}

cnt--;
if(rank[p1]<rank[p2]){
parents[p1]=p2;
}
else if(rank[p2]<rank[p1]){
parents[p2]=p1;
}
else if(rank[p1]==rank[p2]){
parents[p1]=p2;//两个集合的秩(深度)相同时,新的代表是哪个就不重要了(因为路径压缩的效率只与深度有关,所以相等深度的两个旧集合哪个宽度更大并不重要)
rank[p2]++;//新的集合的秩(深度)要+1;
}
return ;
}
int main()
{
while(~scanf("%d%d",&n,&m)&&n!=0){
cnt=n;
for(int i=1;i<=n;i++){
parents[i]=i;
rank[i]=0;
}

int a;
int b;
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
Join(a,b);
}

cout<<"Case "<<++kase<<": "<<cnt<<endl;
}

return 0;
}


理解不同之处,欢迎指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: