POJ 1703 Find them, Catch them (并查集)

题目的难点是怎样保存 D i j 这条信息。本质上就是把两个集合并起来,一个集合是现在已经知道的跟i相关的罪犯,另外一集合是现知的跟j相关的罪犯,当然这两个集合有可能是同一个集合(此时i,j之间的关系在此信息之前就已经确定了)。这样所有的罪犯分成若干个集合,每个罪犯属于且仅属于一个集合。对于每条信息D i j,我们都把包含i的集合和包含j的集合并成一个集合。

我们采取森林的方法来保存这些集合。对于属于同一颗树的所有节点,划分一个集合。这样两个集合之间的并就简化为了两棵树之间的合并了。

为了查询i所在的树,不断查找其父节点,最后找到i所在树的头节点即可。i,j在同一颗树上的充分必要条件就是他们的头节点一样。而D i j 即是将一个头节点连到另外一个头节点上去(如图所示)。

至此为止,我们保存了对于任何i,j,他们是否相关(即是否可以判断是否在同一个帮派,其依据就是是否在同一颗树上)的信息。

我们还需要判断对于同一颗树上的i,j,他们是否属于同一个帮派。注意到上面我们的判断准则,对于树的一条边,需要保存两个端点是否在同一个帮派的信息,称为权,比如用0表示是同一个帮派,反之用1表示。此时对于从i到j的一条路径,将所有边上的权相加,若为奇数则不在同一个帮派,反之则在同一个帮派。

每一条边的权可以保存在下面那个端点的信息里面。

几个细节和注意事项:

1. 罪犯编号从1开始,方便起见,储存也从1开始。

2. 一开始要将所有父节点位置清零。

3. 最重要的一点是,需要充分发挥树的作用,不要使得成为一个链表。即需要使得树的层数尽量少,这样可以大大降低查找每个点所在树的头节点的时间复杂度,比如说若为链表则为O(n),若为一个完全二叉树则为O(logn)。具体方法是对于i,j查找树的头节点的时候,记下它们到各自头节点的层数,然后将层数少的那个树连到层数较多的那棵树上(如上图)。不过在我的程序中使用权作为层数来用的,也能过J。

具体实现和细节上的处理见后附程序。

数据结构:

对于每个点需要保存两个信息,一个是其父节点,另一个则为它到其父节点那条边的权。所以我们用一个结构体来保存每个罪犯节点。

struct Crime{

int parent;// 父节点位置

int weight; // 权

};

然后用一个数组来表示100000个罪犯:

Crime crime[100001];//注意程序中罪犯编号从1开始。

时空分析:

时间复杂度:

最坏情形为O(m*n),平均情况为O(mlogn),后者是可以接受的。

空间复杂度:

用了一个大数组,为O(n)。

源程序: 

 

方法一:
 #include<iostream>
#include<cstdio>
#include
<cstring>
using namespace std;
#define MAXN 100001
struct Crime
{
int parent ,weight;
};
Crime crime[MAXN];
int n,m;
int main()
{
int T,one,two;
char order[1000];
scanf(
"%d",&T);
while(T--)
{
scanf(
"%d%d",&n,&m);
memset(crime,
0,sizeof(crime));
while(m--)
{
scanf(
"%s%d%d",order,&one,&two);
if(order[0]=='D')
{
int temp1=one,temp2=two,t1=0,t2=0;
while(crime[temp1].parent!=0)
{
t1
+=crime[temp1].weight;
temp1
=crime[temp1].parent;
}
// 查找one 所在树的头节点,并计算到头节点的权之和
while(crime[temp2].parent !=0)
{
t2
+=crime[temp2].weight;
temp2
=crime[temp2].parent;
}
// 查找 two 所在树的头节点,并计算到头节点的权之和
if(temp1!=temp2)
{
if(t1<t2)
{
crime[temp1].parent
=temp2;
crime[temp1].weight
=(t1-t2+1)%2;
}
// 若t1<t2 ,把 one 所在树挂到 two上
else
{
crime[temp2].parent
=temp1;
crime[temp2].weight
=(t1-t2+1)%2;
}
// 反之,将 two 所在树挂到 one 上。
}
}
else
{

int temp1=one,temp2=two,t1=0,t2=0;
while(crime[temp1].parent!=0)
{
t1
+=crime[temp1].weight;
temp1
=crime[temp1].parent;
}
// 查找one 所在树的头节点,并计算到头节点的权之和
while(crime[temp2].parent!=0)
{
t2
+=crime[temp2].weight;
temp2
=crime[temp2].parent;
}
// 查找 two 所在树的头节点,并计算到头节点的权之和
if(temp1!=temp2) // 不在同一棵树
printf("Not sure yet.\n");
else if((t1-t2)%2==0) // 权相差为偶数
printf("In the same gang.\n");
else printf("In different gangs.\n");// 权相差为一个奇数
}
}
}
return 0;
}

方法二:
源代码:(944K 407MS) // 若加上按秩合并,则为复杂度为(1332K 360MS)
#include<iostream>
using namespace std;
const int Max = 100050;

int n, m;
int parent[Max], opp[Max]; 
// 用于记录x1个不同帮派opp[x],若没有x的信息则初始化为opp[x] = 0。

void make_set(){
for(int x = 1; x <= n; x ++){
parent[x]
= x;
opp[x]
= 0;
}
}

int find_set(int x){
if(x != parent[x])
parent[x]
= find_set(parent[x]);
return parent[x];
}

void union_set(int x, int y){
x
= find_set(x);
y
= find_set(y);
if(x == y) return;
parent[y]
= x;
}

int main(){
int t, x, y;
scanf(
"%d", &t);
while(t --){
scanf(
"%d %d", &n, &m);
getchar();
make_set();
while(m --){
char c;
scanf(
"%c %d %d", &c, &x, &y);
getchar();
// 记得要回收回车号。
if(c == 'D'){
if(opp[x] == 0 && opp[y] == 0){ // 情况1:x,y在前面都没有信息。
opp[x] = y;
opp[y]
= x;
}
else if(opp[x] == 0){ // 情况2:x在前面都没有信息,而y有。
opp[x] = y;
union_set(x, opp[y]);
}
else if(opp[y] == 0){ // 情况3:y在前面都没有信息,而x有。
opp[y] = x;
union_set(y, opp[x]);
}
else{ // 情况4:x,y在前面都有信息。
union_set(x, opp[y]);
union_set(y, opp[x]);
}
}
if(c == 'A'){ // 注意三种情况的判断依据。
if(find_set(x) == find_set(y))
printf(
"In the same gang.\n");
else if(find_set(x) == find_set(opp[y]))
printf(
"In different gangs.\n");
else
printf(
"Not sure yet.\n");
}
}
}
return 0;
}

并查集

并查集的一般用途就是用来维护某种具有自反、对称、传递性质的关系的等价类。并查集一般以树形结构存储,多棵树构成一个森林,每棵树构成一个集合,树中的每个节点就是该集合的元素,找一个代表元素作为该树(集合)的祖先。

并查集支持以下三种操作:

1Make_Set(x) 把每一个元素初始化为一个集合

初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身。

2Find_Set(x) 查找一个元素所在的集合

查找一个元素所在的集合,只要找到这个元素所在集合的祖先即可。判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。

3Union(x,y) 合并x,y所在的两个集合

合并两个不相交集合操作很简单:首先设置一个数组Father[x],表示x"父亲"的编号。那么,合并两个不相交集合的方法就是,找到其中一个集合的祖先,将另外一个集合的祖先指向它。

并查集的优化

1Find_Set(x)时 路径压缩

寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,有没有办法减小这个复杂度呢?

答案是肯定的,这就是路径压缩,即当我们经过"递推"找到祖先节点后,"回归"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了。

2Union(x,y)时 按秩合并

即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。

主要代码实现

/* father[x]表示x的父节点 */
int father[MAX];

/* rank[x]表示x的秩 */
int rank[MAX];

/* 初始化集合 */
void Make_Set(int x)
{
father[x]
= x;
rank[x]
= 0;
}

/* 查找x元素所在的集合,回溯时压缩路径 */
int Find_Set(int x)
{
if (x != father[x])
{
father[x]
= Find_Set(father[x]);
}
return father[x];
}

/* 按秩合并x,y所在的集合 */
void Union(int x, int y)
{
x
= Find_Set(x);
y
= Find_Set(y);
if (x == y) return;
if (rank[x] > rank[y])
{
father[y]
= x;
}
else
{
if (rank[x] == rank[y])
{
rank[y]
++;
}
father[x]
= y;
}
}
我的代码:
#include<cstdio>
#include
<iostream>
using namespace std;
#define MAXN 100000
int father[MAXN];
int weight[MAXN];
void make_set(int x)
{
father[x]
=x;
weight[x]
=0;
}
int find_set(int x)
{
int t;
if(x==father[x]) return x;
else
{
t
=father[x];
father[x]
=find_set(father[x]);
weight[x]
=(weight[x]+weight[t])%2;
return father[x];
}
}
void union_set (int x,int y)
{
int rx=find_set(x);
int ry=find_set(y);
if(rx==ry) return ;

father[rx]
=ry;
weight[rx]
=(weight[y]+1-weight[x]+2)%2;
}
int main()
{
int x,y,rx,ry,T,n,m,i;
char s[110];
scanf(
"%d",&T);
while(T--)
{
scanf(
"%d%d",&n,&m);
for(i=1;i<=n;i++)
make_set(i);
while(m--)
{
scanf(
"%s%d%d",s,&x,&y);
if(s[0]=='D')
union_set(x,y);
else
{
rx
=find_set(x);
ry
=find_set(y);
if(rx!=ry) printf("Not sure yet.\n");
else if((weight[x]-weight[y]+2)%2==0)
printf(
"In the same gang.\n");
else
printf(
"In different gangs.\n");
}
}
}
return 0;
}


posted on 2011-06-04 00:13  thinking001  阅读(125)  评论(0)    收藏  举报

导航