并查集基础与习题

目录 并查集 例题: A:POJ-2236 Wireless Network B:POJ-1611 The Suspects C:HDU-1213 H

目录

并查集

例题:

A:POJ-2236 Wireless Network

B:POJ-1611 The Suspects

C:HDU-1213 How Many Tables

D:HDU-3038 How Many Answers Are Wrong

E:POJ-1182 食物链

F:POJ-1456 Supermarket

G:POJ-1733 Parity game

H:POJ-1984 Navigation Nightmare

I:POJ-2492 A Bug's Life

J:POJ-2912 Rochambeau

K:ZOJ-3261 Connections in Galaxy War

L:HDU-1272 小希的迷宫

M:POJ-1308 Is It A Tree?


先推荐大家去看个视频并查集视频(看完就不用看我的讲解了,直接看习题)

并查集转自(并查集详解

一、问题引入

原题:杭电hdu1232畅通工程 

题意:首先在地图上给你若干个城镇,这些城镇都可以看作点,然后告诉你哪些对城镇之间是有道路直接相连的。最后要解决的是整幅图的连通性问题。比如随意给你两个点,让你判断它们是否连通,或者问你整幅图一共有几个连通分支,也就是被分成了几个互相独立的块。像畅通工程这题,问还需要修几条路,实质就是求有几个连通分支。如果是1个连通分支,说明整幅图上的点都连起来了,不用再修路了;如果是2个连通分支,则只要再修1条路,从两个分支中各选一个点,把它们连起来,那么所有的点都是连起来的了;如果是3个连通分支,则只要再修两条路……

说明:输入4 2   1 3   4 3。即一共有4个点,2条路。下面两行告诉你,1、3之间有条路,4、3之间有条路。那么整幅图就被分成了1-3-4和2两部分。只要再加一条路,把2和其他任意一个点连起来,畅通工程就实现了,那么这个这组数据的输出结果就是1。好了,现在编程实现这个功能吧,城镇有几百个,路有不知道多少条,而且可能有回路。 这可如何是好? 我以前也不会呀,自从用了并查集之后,嗨,效果还真好!

 

二、故事描述

并查集由一个整数型的数组和两个函数构成。数组pre[]记录了每个点的前导点是什么,函数find是查找,函数join是合并。

话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的群落,通过两两之间的朋友关系串联起来。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢? 我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物,这样,每个圈子就可以这样命名“齐达内朋友之队”“罗纳尔多朋友之队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。 但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长,要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?” 这样一来,队长面子上挂不住了,而且效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否连通,至于他们是如何连通的,以及每个圈子内部的结构是怎样的,甚至队长是谁,并不重要。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。  

下面我们来看并查集的实现。 int pre[1000];  这个数组,记录了每个大侠的上级是谁。大侠们从1或者0开始编号(依据题意而定),pre[15]=3就表示15号大侠的上级是3号大侠。如果一个人的上级就是他自己,那说明他就是掌门人了,查找到此为止。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。每个人都只认自己的上级。比如胡青牛同学只知道自己的上级是杨左使。张无忌是谁?不认识!要想知道自己的掌门是谁,只能一级级查上去。 find这个函数就是找掌门用的,意义再清楚不过了(路径压缩算法先不论,后面再说)。

int find(int x)                        //查找x的掌门
{
    int r=x;                           //委托 r 去找掌门
    while(pre[r] != r)                 //如果r的上级不是r自己(也就是说找到的大侠他不是掌门 = =)
        r = pre[r] ;                   // r 接着找他的上级,直到找到掌门为止。
    return  r ;                        //掌门驾到~~~
}

再来看看join函数,就是在两个点之间连一条线,这样一来,原先它们所在的两个板块的所有点就都可以互通了。这在图上很好办,画条线就行了。但我们现在是用并查集来描述武林中的状况的,一共只有一个pre[]数组,该如何实现呢? 还是举江湖的例子,假设现在武林中的形势如图所示。虚竹小和尚与周芷若MM是我非常喜欢的两个人物,他们的终极boss分别是玄慈方丈和灭绝师太,那明显就是两个阵营了。我不希望他们互相打架,就对他俩说:“你们两位拉拉勾,做好朋友吧。”他们看在我的面子上,同意了。这一同意可非同小可,整个少林和峨眉派的人就不能打架了。这么重大的变化,可如何实现呀,要改动多少地方?其实非常简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先的所有人员的终极boss都是师太,那还打个球啊!反正我们关心的只是连通性,门派内部的结构不要紧的。”玄慈一听肯定火大了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”抗议无效,上天安排的,最大。反正谁加入谁效果是一样的,我就随手指定了一个。这段函数的意思很明白了吧?  

void join(int x,int y)                     //我想让虚竹和周芷若做朋友
{
    int fx=find(x), fy=find(y);             //虚竹的老大是玄慈,芷若MM的老大是灭绝
    if(fx != fy)                             //玄慈和灭绝显然不是同一个人
        pre[fx]=fy;                       //方丈只好委委屈屈地当了师太的手下啦
}

再来看看路径压缩算法。建立门派的过程是用join函数两个人两个人地连接起来的,谁当谁的手下完全随机。最后的树状结构会变成什么样,我也完全无法预计,一字长蛇阵也有可能。这样查找的效率就会比较低下。最理想的情况就是所有人的直接上级都是掌门,一共就两级结构,只要找一次就找到掌门了。哪怕不能完全做到,也最好尽量接近。这样就产生了路径压缩算法。 设想这样一个场景:两个互不相识的大侠碰面了,想知道能不能揍。 于是赶紧打电话问自己的上级:“你是不是掌门?” 上级说:“我不是呀,我的上级是谁谁谁,你问问他看看。” 一路问下去,原来两人的最终boss都是东厂曹公公。 “哎呀呀,原来是记己人,西礼西礼,在下三营六组白面葫芦娃!” “幸会幸会,在下九营十八组仙子狗尾巴花!” 两人高高兴兴地手拉手喝酒去了。 “等等等等,两位同学请留步,还有事情没完成呢!”我叫住他俩。 “哦,对了,还要做路径压缩。”两人醒悟。 白面葫芦娃打电话给他的上级六组长:“组长啊,我查过了,其习偶们的掌门是曹公公。不如偶们一起直接拜在曹公公手下吧,省得级别太低,以后查找掌门麻环。” “唔,有道理。” 白面葫芦娃接着打电话给刚才拜访过的三营长……仙子狗尾巴花也做了同样的事情。 这样,查询中所有涉及到的人物都聚集在曹公公的直接领导下。每次查询都做了优化处理,所以整个门派树的层数都会维持在比较低的水平上。路径压缩的代码,看得懂很好,看不懂也没关系,直接抄上用就行了。总之它所实现的功能就是这么个意思。

 

三、算法描述

关键特征:

①用集合中的某个元素来代表这个集合,该元素称为集合的代表元

②一个集合内的所有元素组织成以代表元为根的树形结构;

③对于每一个元素 pre[x]指向x在树形结构上的父亲节点。如果x是根节点,则令pre[x] = x;

④对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着pre[x]不断在树形结构中向上移动,直到到达根节点。

判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可。

路径压缩:  

为了加快查找速度,查找时将x到根节点路径上的所有点的pre(上级)设为根节点,该优化方法称为压缩路径。使用该优化后,平均复杂度可视为Ackerman函数的反函数,实际应用中可粗略认为其是一个常数。

用途:

1、维护无向图的连通性。支持判断两个点是否在同一连通块内,和判断增加一条边是否会产生环。

2、用在求解最小生成树的Kruskal算法里。

一般来说,一个并查集对应三个操作:初始化+查找根结点函数+合并集合函数

【初始化】

包括对所有单个的数据建立一个单独的集合(即根据题目的意思自己建立的最多可能有的集合,为下面的合并查找操作提供操作对象)。

在每一个单个的集合里面,有三个东西。

①集合所代表的数据(这个初始值根据需要自己定义,不固定) ;

②这个集合的层次通常用rank表示(一般来说,初始化的工作之一就是将每一个集合里的rank置为1);

③这个集合的类别pre(其实就是一个指针,用来指示这个集合属于那一类,合并过后的集合,他们的pre指向的最终值一定是相同的) (有的简单题里面集合的数据就是这个集合的标号,也就是说只包含2和3,1省略了)。

初始化的时候,每一个集合的pre都是这个集合自己的标号。没有跟它同类的集合,那么这个集合的源头只能是自己了。

最简单的集合就只含有这三个东西了,当然,复杂的集合就是把3指针这一项添加内容,如PKU食物链那题,我们还可以添加enemy指针,表示这个物种集合的天敌集合;food指针,表示这个物种集合的食物集合。随着指针的增加,并查集操作起来也变得复杂,题目也就显得更难了。

数组表示法

设置很多相同大小的数组,如:

int pre[max];   //集合index的类别,或者用parent表示
int rank[max];  //集合index的层次,通常初始化为0
int data[max];  //集合index的数据类型 
//初始化集合
void Make_pre(int i)
{    
    pre[i]=i;   //一个集合的pre都是这个集合自己的标号。没有跟它同类的集合,那么这个集合的源头只能是自己了。 
    rank[i]=0;
}

【查找函数】

就是找到pre指针的源头,可以把函数命名为find_pre,如果集合的pre等于集合的编号(即还没有被合并或者没有同类),那么自然返回自身编号。 如果不同(即经过合并操作后指针指向了源头(合并后选出的rank高的集合))那么就可以调用递归函数,如下面的代码:

//查找集合i(一个元素是一个集合)的源头(递归实现)
int Find_pre(int i)
{
    //如果集合i的父亲是自己,说明自己就是源头,返回自己的标号
   if(pre[i]==i)
       return pre[i];
    //否则查找集合i的父亲的源头
    return  Find_pre(pre[i]);       
}

【合并集合函数】

这就是所谓并查集的并了。至于怎么知道两个集合是可以合并的,那就是题目的条件了。先看代码:

void Union(int i,int j)
{
    i=Find_pre(i);
    j=Find_pre(j);
    if(i==j) return ;
    if(rank[i]>rank[j]) pre[j]=i;
    else
    {
        if(rank[i]==rank[j]) rank[j]++;  
        pre[i]=j;
    }
}

四、代码实现

#define N 105
int pre[N];     //每个结点
int rank[N];    //树的高度
//初始化
int init(int n)     //对n个结点初始化
{
    for(int i = 0; i < n; i++){
        pre[i] = i;     //每个结点的上级都是自己
        rank[i] = 1;    //每个结点构成的树的高度为1
    }
}
 
int find_pre(int x)     //查找结点x的根结点
{
    if(pre[x] == x){        //递归出口:x的上级为x本身,即x为根结点
        return x;      
    }
    return find_pre(pre[x]);    //递归查找
}
 
//改进查找算法:完成路径压缩,将x的上级直接变为根结点,那么树的高度就会大大降低
int find_pre(int x)     //查找结点x的根结点
{
    if(pre[x] == x){        //递归出口:x的上级为x本身,即x为根结点
        return x;      
    }
    return pre[x] = find_pre(pre[x]);   //递归查找  此代码相当于 先找到根结点rootx,然后pre[x]=rootx
}
 
 
bool is_same(int x, int y)      //判断两个结点是否连通
{
    return find_pre(x) == find_pre(y);  //判断两个结点的根结点(亦称代表元)是否相同
}
 
void unite(int x,int y)
{
    int rootx, rooty;
    rootx = find_pre(x);
    rooty = find_pre(y);
    if(rootx == rooty){
        return ;
    }
    if(rank(rootx) > rank(rooty)){
        pre[rooty] = rootx;         //令y的根结点的上级为rootx
    }
    else{
        if(rank(rootx) == rank(rooty)){
            rank(rooty)++;
        }
        pre[rootx] = rooty;
    }
}

例题:

A:POJ-2236 Wireless Network:题目大意是说有一些电脑,编号为1到N,现在这些电脑坏了,无法相互连通,我们需要维修,输入首先输入N和d,N表示有多少台电脑,d表示两台已维修好的电脑若它们之间的距离小于等于d,则两台电脑可以互通。接下来输入N行,每行输入a,b两个数,N行中的第i行表示编号为i的电脑的坐标(用来求两台电脑的距离),在接下来的输入各种操作,O a表示编号为a的电脑被维修好了,S a b则表示询问编号为a和b的电脑能不能互通,若能则输出SUCCESS,若不能则输出FAIL。简单的并查集问题。AC代码:

#include <iostream>
#include <cmath>
using namespace std;

struct coordinate{
	int x,y;
}coo[1010];
int set1[1010] = { 0 }, map1[1010][1010] = { 0 };
int n,d,x,y,i,j;
string s;

int FindSet(int x){
	if (set1[x]!=x)
		set1[x]=FindSet(set1[x]);
	return set1[x];
}
void Union(int x, int y){
	y = FindSet(y);
	set1[y] = x;
	return;
}
int main()
{
	cin>>n>>d;
	for (i=1;i<=n;++i)
		cin>>coo[i].x>>coo[i].y;
	for (i=1;i<n;++i)
		for (j=i+1;j<=n;++j)
			if (sqrt((coo[i].x-coo[j].x)*(coo[i].x-coo[j].x) + (coo[i].y-coo[j].y)*(coo[i].y-coo[j].y))<=d){
				map1[i][j] = 1;
				map1[j][i] = 1;
			}
	while (cin>>s)
		if (s == "O"){
			cin>>x;
			set1[x]=x;
			for (i=1;i<=n;++i)
				if((map1[x][i]==1)&&(set1[i]))  //set1[i]不为0即已经修好
					Union(x,i);
		}
		else{
			cin>>x>>y;
			if (FindSet(x)==FindSet(y))
				cout<<"SUCCESS\n";
			else
				cout<<"FAIL\n";
		}
	return 0;
}

B:POJ-1611 The Suspects:该题的意思是,给你几个圈子,把与0直接相关和间接相关的圈子都找出来,输出这些圈子有多少个人。简单的模板题,看代码即可:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define maxn 30001
int set1[maxn];
int sum[maxn];
int findset(int x)
{
    if(set1[x]!=x)
      set1[x]=findset(set1[x]);
    return set1[x];
}
int main()
{
    int x,y,m,n,w,i;
    while(scanf("%d%d",&m,&n)&&(m!=0||n!=0))
    {
        memset(sum,0,sizeof(sum));
        for(i=0;i<m;i++){
            set1[i]=i;
            sum[i]=1;
        }
        for(i=1;i<=n;i++){
            scanf("%d",&w);
            if(w>0){
                scanf("%d",&x);
                w--;
            }
            while(w--){
                scanf("%d",&y);
                x=findset(x);
                y=findset(y);
                if(x!=y){
                    set1[x]=y;
                    sum[y]+=sum[x];
                }
            }
        }
        /*for(i=0;i<m;i++)
            if(findset(i)==findset(0))
               k++;
        printf("%d\n",k);*/
        printf("%d\n",sum[findset(0)]);
    }
    return 0;
}

C:HDU-1213 How Many Tables:题目大致意思是,有n个客人,有m组两个客人之间的关系,代表两个客人之间相互认识,然后要给客人们安排桌子,客人们与其他人坐同一张桌子的条件是这张桌子上至少有一个人是他认识的,问你最少要安排多少张桌子。比如第一组样例,1和2认识,2又和3认识,那么1,2,3就可以坐同一张桌子,而4,5和1,2,3互相不认识,所以他们不能坐同一张桌子,而4,5相互认识,所以4和5可以坐同一张桌子,所以总共需要两张桌子。又是一道模板题,代码:

#include<bits/stdc++.h>
using namespace std;
#define maxn 30001
int set1[maxn];
int sum[maxn];
int findset(int x)
{
    if(set1[x]!=x)
      set1[x]=findset(set1[x]);
    return set1[x];
}
int main()
{
    int t,x,y,m,n;
    cin>>t;
    while(t--)
    {
        int ans=0;
        cin>>n>>m;
        for(int i=0;i<=n;i++)
            set1[i]=i;
        for(int i=1;i<=m;i++){
            cin>>x>>y;
            x=findset(x);
            y=findset(y);
            if(x!=y){
                set1[x]=y;
            }
        }
        for(int i=1;i<=n;i++)
            if(set1[i]==i)
                ans++;
        cout<<ans<<endl;
    }
    return 0;
}

D:HDU-3038 How Many Answers Are Wrong:给出一个数组的区间和,如果后面与前面矛盾认为是假话。带权并查集判错问题,很经典的问题,后面好多题和本题也是一个思路。可以先看这个题解给出的思路和推出的公式,后面很多题都会用到,代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define maxn 200000+10
int pre[maxn],rank[maxn];//rank[]表示到根节点的距离
int n,m,sum;
 
void init()
{
    for(int i=0; i<=n; i++)
    {
        pre[i]=i;
        rank[i]=0;
    }
    sum=0;
}
 
int Find(int x)
{
    int temp=pre[x];
    if(x==pre[x])
        return x;
    pre[x]=Find(temp);
    rank[x]=rank[x]+rank[temp];
    return pre[x];
}
 
void join(int x,int y,int k)
{
    int fx=Find(x),fy=Find(y);
    if(fx!=fy)
    {
        pre[fx]=fy;
        rank[fx]=rank[y]+k-rank[x];
    }
    else
    {
        if(rank[x]-rank[y]!=k)
            sum++;
    }
}
 
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        init();
        int x,y,z,i;
        for(i=1; i<=m; i++)
        {
            scanf("%d%d%d",&x,&y,&z);
            join(x-1,y,z);
        }
        printf("%d\n",sum);
    }
    return 0;
}

E:POJ-1182 食物链:就是一个思维题,具体看代码:

/*一定要仔细读题读题读题啊,A吃B B吃C C吃A这是规则!!!一定要遵循的,A吃B,B吃C,C一定吃A。唯一需要注意的地方就是这个规则,其他的按照并查集思想很容易解决。*/
#include <iostream>
#include <cstdio>
#include <string>
#include <algorithm>
#include <queue>
#include <map>
#include <cstring>
#include <cmath>

const int maxn=50005;

using namespace std;

int n,k,father[3*maxn];		//数组开大点来存储同类,吃,被吃的关系。
							//father[a]存同类,father[a+n]存吃哪种种类,father[a+2*n]存被哪种种类吃

int Find(int x)
{
    return x == father[x] ? x : Find(father[x]);
}

int main()
{
    cin >> n >> k;
    int d,x,y,num=0;
    for(int i=1;i<=3*n;i++){
        father[i]=i;
    }
    for(int i=1;i<=k;i++){
        scanf("%d%d%d",&d,&x,&y);
        if(x>n || y>n){
            num+=1;
        }
        else if(d == 1){
            if(Find(x)==Find(y+n) || Find(x)==Find(y+2*n))
                num+=1;
            else{							//所有的吃与被吃,x与y要统一
                father[Find(x+n)]=Find(y+n);
                father[Find(x)]=Find(y);
                father[Find(x+2*n)]=Find(y+2*n);
            }
        }
        else{
            if(Find(x+n)==Find(y+n) || Find(x+n)==Find(y+2*n) || Find(x)==Find(y) || Find(x)==Find(y+n) || Find(x+2*n)==Find(y) || Find(x+2*n)==Find(y+2*n))
                num+=1;
            else{  							//遵循A吃B,B吃C,C吃A的规则进行修改x。
                father[Find(x+n)]=Find(y);
                father[Find(x+2*n)]=Find(y+n);
                father[Find(x)]=Find(y+2*n);
            }
        }
    }
    printf("%d\n",num);
    return 0;
}

F:POJ-1456 Supermarket:超市里有N件商品,每个商品都有利润pi和过期时间di,每天只能卖一件商品,过期商品(即当天di<=0)不能再卖。求合理安排每天卖的商品的情况下,可以得到的最大收益是多少。这个题可以用贪心,并查集,优先队列做,具体看这里,只给出并查集代码,思路为用并查集将时间加入集合中,先令pre[dx]=dx,如果这一天要被占用就将其pre[dx]改为pre[dx]=dx-1,表示日期和dx相同的物品最晚只能从dx-1天售卖。代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;

#define maxn 10000+10
struct node
{
    int px,dx;
    friend bool operator<(node a,node b)
    {
        return a.px>b.px;
    }
}q[maxn];
int pre[maxn];

void init(int n)
{
    for(int i=0;i<=n;i++)
        pre[i]=i;
}

int Find(int x)
{
    if(x!=pre[x])
        pre[x]=Find(pre[x]);
    return pre[x];
}

int main()
{
    int n;
    while(~scanf("%d",&n))
    {
        int maxx=0,i;
        for(i=0;i<n;i++)
        {
            scanf("%d%d",&q[i].px,&q[i].dx);
            if(q[i].dx>maxx)
                maxx=q[i].dx;
        }
        init(maxx);
        sort(q,q+n);
        int sum=0;
        for(i=0;i<n;i++)
        {
            int k=Find(q[i].dx);
            if(k>0)
            {
                sum+=q[i].px;
                pre[k]=k-1;
            }
        }
        printf("%d\n",sum);
    }
    return 0;
}

G:POJ-1733 Parity game:给了n个描述,从l到r区间内数的和是奇数还是偶数,给出第一个矛盾是哪句话。如果没有矛盾,就是n;思路和D题没有太大区别,就是本题中的区间范围很大,但是输入的点却最多只有5000*2个,所以可以从这里入手,建立映射关系比如建立一个f(x)的映射关系,把每一个输出的点都存入f(x)函数中,所以f(x)函数最多只有5000*2个数,而后我们查询的时候只需要借用他们映射的位置,通过其位置来实现对数据的处理,也就是说我们所用的只是这5000*2个位置与数据的关系,这样就实现了离散化(暂时我的离散化就是这样的),代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
using namespace std;

#define maxn 10000+10
struct node
{
    int u,v,w;
} a[maxn];
int pre[maxn],rank[maxn];
int f[maxn];

void init(int n)
{
    for(int i=0; i<n; i++)
    {
        pre[i]=i;
        rank[i]=0;
    }
}

int Find(int x)
{
    int temp=pre[x];
    if(x==pre[x])
        return x;
    pre[x]=Find(temp);
    rank[x]=(rank[x]+rank[temp])%2;
    return pre[x];
}

int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m))
    {
        int i,cnt=0;
        char s[10];
        for(i=1; i<=m; i++)
        {
            scanf("%d%d %s",&a[i].u,&a[i].v,s);
            a[i].u--;
            if(!strcmp(s,"even"))
                a[i].w=0;
            else
                a[i].w=1;
            f[cnt++]=a[i].u;
            f[cnt++]=a[i].v;
        }
        sort(f,f+cnt);
        n=unique(f,f+cnt)-f;
        init(n);
        int sum=0;
        for(i=1; i<=m; i++)
        {
            int x=lower_bound(f,f+n,a[i].u)-f;//查询其位置
            int y=lower_bound(f,f+n,a[i].v)-f;//
            int fx=Find(x),fy=Find(y);
            if(fx!=fy)
            {
                pre[fx]=fy;
                rank[fx]=(rank[y]+a[i].w-rank[x])%2;//通过其位置来建立关系
            }
            else
            {
                if((abs(rank[x]-rank[y])%2)!=a[i].w)
                    break;
            }
            sum++;
        }
        printf("%d\n",sum);
    }
    return 0;
}

H:POJ-1984 Navigation Nightmare:题意:给你n个点和m操作,每次操作都有a b d op ,表示a点在b点的op方向(有东南西北)距离为d, 那么之后会有K次操作,a b c ,表示在第c次操作之前a和b之间的距离。思路:首先他说的是第c次操作之前,也就是我们没有办法直接把并查集全部的建立好,所以首先我们要把操作存起来, 之后就是如何运用并查集了,首先有四个方向,东南西北,我们可以建立两个并查集一个是东西方向一个是南北方向, 之后就是规定东西和南北方向的正方向,比如我们规定东是正方向那么a在b的南方d米其实就是a在b的北方-d方向, 我们维护两个Rank数组就好了,还有一些具体的细节看代码:

#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <iostream>
using namespace std;
const int maxn = 40000+10;
int p[maxn],D[maxn],B[maxn];

struct node
{
	int x,y,d;
	char op[5];
}edg[maxn];

void init()
{
	for(int i = 0 ; i < maxn ; i ++){
		p[i] = i ;
		D[i] = 0 ;
		B[i] = 0 ;
		edg[i].x = 0 , edg[i].y = 0 ,edg[i].d = 0 ;
	}
}
int getf(int x)
{
	if(x == p[x])
		return p[x];
	int t = p[x];
	p[x] = getf(p[x]);
	D[x] += D[t];
	B[x] += B[t];
	return p[x];
}
// 东 east 南 south 西 west 北 north
void merge(int x,int y,int d,char opr[])
{
	int dx = getf(x);
	int dy = getf(y);
	if(dx != dy){
		p[dy] = dx;
		if(opr[0] == 'E'||opr[0] == 'W'){//这里虽然是在东西方向,但是南北方向的权值数组也是要更新的因为a在b的正西方向d米
                                        //其实是a在b东西方向,向西移动d米加上a在b南北方向上移动0米
			if(opr[0] == 'E')D[dy] = D[x] - D[y] + d;//如果和正方向相同就是加上这个距离
			if(opr[0] == 'W')D[dy] = D[x] - D[y] - d;// 减去这个距离
			B[dy] = B[x] - B[y];//什么都不加
		}
		else if(opr[0] == 'N'||opr[0] == 'S'){
			if(opr[0] == 'N') B[dy] = B[x] - B[y] + d;
			if(opr[0] == 'S') B[dy] = B[x] - B[y] - d;
			D[dy] = D[x] - D[y];
		}


	}
}
int main()
{
	int n,m;
	while(scanf("%d%d",&n,&m)!=EOF){
		init();
		for(int i = 1 ; i <= m ; i++)
			cin>>edg[i].x>>edg[i].y>>edg[i].d>>edg[i].op;
		int t,a,b,cnt = 1,c;
		int ans;
		scanf("%d",&t);
		for(int i = 0 ; i < t ; i ++){//这里他输入的c其实就是按照从小到大的方式输入的所以我们可以边输入边建立
			scanf("%d%d%d",&a,&b,&c);
			while(cnt<=c){//在c之前的我们都给他建立了
				merge(edg[cnt].x,edg[cnt].y,edg[cnt].d,edg[cnt].op);
				cnt++;
			}
			int da = getf(a);
			int db = getf(b);
			if(da != db) ans = -1;
			else{
				ans = abs(D[a] - D[b]) + abs(B[a] - B[b]);
			}
			printf("%d\n",ans);
		}
	}
}

I:POJ - 2492 A Bug's Life:并查集问题,输入互相交配的昆虫,找是否存在同性恋的昆虫,在这里会将昆虫看作节点。把同性的放在一个集合内,若检测到有异性存在同一集合内则有同性恋,AC代码:

#include <cstdio>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
 
const int MAXN = 2010;
int set[MAXN<<1];
 
int find(int p)
{
    if(set[p] < 0) return p;
    return set[p] = find(set[p]);
}
 
void join(int p, int q)
{
    p = find(p); q = find(q);
    if(p != q) set[p] = q;
}
 
int main()
{
    int t, m, n, w = 1;
    scanf("%d", &t);
    while(t--)
    {
        memset(set, -1, sizeof(set));
        scanf("%d%d" , &n, &m);
        bool flag = false;//没有矛盾情况
        while(m--)
        {
            int a, b;
            scanf("%d%d", &a, &b);
            join(a, b+n);
            join(b, a+n);
            if(find(a)==find(a+n) || find(b)==find(b+n))
                flag = true;
        }
        if(1 != w) printf("\n");
        printf("Scenario #%d:\n", w++);
        printf("%s\n", flag ? "Suspicious bugs found!" : "No suspicious bugs found!");
    }
    return 0;
}

J:POJ-2912 Rochambeau:有几个小孩在做石头剪刀布游戏,他们当中有一个裁判(裁判可以随机出,其他人只能出一种),下面给出几组刚开始他们的胜负情况,让你判断谁是裁判,并输出最少的步数。思路:用种类并查集存储各个人之间的关系,然后从0编号开始假设其为裁判,若其中有一步与前面冲突,则代表他不是裁判,依次类推,这道题关键是每次判断一个人是不是裁判时,需要初始化pre,与v数组。代码:

#include <iostream>
#include<stdio.h>
#include<algorithm>
#include<string.h>
#define MAX 2010
using namespace std;
int v[MAX],pre[MAX];
int n,m;
char cc;
int a,b;
struct node
{
    int u,v,w;
} k[MAX];
void init()
{
    for(int i=0; i<=n; i++)
    {
        pre[i]=i;
        v[i]=0;
    }
}
int fa(int root)
{
    if(root==pre[root])
        return root;
    int tep=fa(pre[root]);
    v[root]=(v[root]+v[pre[root]]+3)%3;
    pre[root]=tep;
    return tep;
}
int judge(int aa,int bb,int c)
{
    int root1=fa(aa);
    int root2=fa(bb);
    if(root1==root2)
    {
        if((v[aa]-v[bb]+3)%3==c)
            return 1;
        else
            return 0;
    }
    else
    {
        pre[root1]=root2;
        v[root1]=(v[bb]+c-v[aa]+3)%3;
        return 1;
    }
}
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=1; i<=m; i++)
        {
            scanf("%d%c%d",&a,&cc,&b);
            k[i].u=a;
            k[i].v=b;
            if(cc=='=')
                k[i].w=0;
            else if(cc=='>')
                k[i].w=1;
            else
                k[i].w=2;
        }
        int kk,ans=0;
        int dis[MAX];
        memset(dis,0,sizeof(dis));
        for(int i=0; i<n; i++)
        {
            init();
            int flag=0;
            for(int j=1; j<=m; j++)
            {
                if(k[j].u==i||k[j].v==i)
                    continue;
                else
                {
                    if(!judge(k[j].u,k[j].v,k[j].w))
                    {
                        dis[i]=j;
                        flag=1;
                        break;
                    }
                }
            }
            if(!flag)
            {
                ans++;
                kk=i;
            }
        }
        if(ans==0)
            printf("Impossible\n");
        else if(ans>=2)
            printf("Can not determine\n");
        else
        {
            int maxx=-1;
            for(int i=0; i<n; i++)
                if(dis[i]>maxx)
                    maxx=dis[i];
            printf("Player %d can be determined to be the judge after %d lines\n",kk,maxx);
        }
    }
 
    return 0;
}

K:ZOJ-3261 Connections in Galaxy War:n个星球组成一个连接的网络,把星球看成点,通讯连线看成边相连,接下来有m个操作,有查询操作,添加边的操作和销毁边的操作,同时每个星球有一个权值,对一个点的查询操作返回的是与它间接或直接相连的星球取权值最大者的序号(权值要比这个点的权值大)。传统的做法是先把输入的点加入并查集建立关系,然后开始判断询问,遇到destroy就将那两个点之间的关系断开,但是很明显,这样很难做到将其关系断开 因此,我们可以逆向想一下了,我们可以先把所有的输入数据都存起来,然后先把未destroy的点与点之间建立关系,这样我们就可以从最后一个询问开始判断并记录下结果,当遇到destroy时再把这两个点建立关系,这样destroy前面的询问就可以正确地判断了。代码:

#include<iostream>
#include<algorithm>
#include<map>
#include<stdio.h>
#include<string.h>
#define N 10005
#define M 20005
#define Q 50005
using namespace std;
int set1[N];
int ans[Q];
//星球
struct node{
	int id;
	int power;
}star[N];
//隧道
struct link{
	int x;
	int y;
}tunnel[M];
//查询
struct ask{
	int x;
	int y;
	char s[10];
}query[Q];
//记录边
map<int, bool> mp;
//查找根节点
int find1(int x){
	return set1[x]==x?x:set1[x]=find1(set1[x]);
//	while(set[x]!=x)
//		x=set[x];
//	return x;
}
void merge1(int a,int b){
	int ax=find1(a);
	int by=find1(b);
	if(ax!=by){
		//能量大的做根节点
		if(star[ax].power>star[by].power)
			set1[by]=ax;
		else if(star[ax].power<star[by].power)
			set1[ax]=by;
		//在能量相同下,编号小的做跟节点
		else{
			if(star[ax].id>star[by].id)
				 set1[ax]=by;
			else
				set1[by]=ax;
		}
	}
}
int main(){
	int n,m,t;
	int flag=1;
	while(cin>>n){
		mp.clear();
		for(int i=0;i<n;i++){
			scanf("%d",&star[i].power);
			star[i].id=i;
			set1[i]=i;
		}
		scanf("%d",&m);
		for(int i=0;i<m;i++){
			scanf("%d%d",&tunnel[i].x,&tunnel[i].y);
			//交换边更有助于判断
			if(tunnel[i].x>tunnel[i].y)
				swap(tunnel[i].x,tunnel[i].y);
			标记没被毁掉的边
			mp[tunnel[i].x*10000+tunnel[i].y]=false;
		}
		scanf("%d",&t);
		for(int i=0;i<t;i++){
			scanf("%s",query[i].s);
			if(query[i].s[0]=='q')
				scanf("%d",&query[i].x);
			else{
				scanf("%d%d",&query[i].x,&query[i].y);
				//交换边更有助于判断
				if(query[i].x>query[i].y)
					swap(query[i].x,query[i].y);
				//标记被毁掉的边
				mp[query[i].x*10000+query[i].y]=true;
			}
		}
		//把没被毁掉的边合并起来
		for(int i=0;i<m;i++)
			if(!mp[tunnel[i].x*10000+tunnel[i].y])
				merge1(tunnel[i].x,tunnel[i].y);
		//逆向查找啦
		int sum=0;
		for(int i=t-1;i>=0;i--)
			if(query[i].s[0]=='q'){
				int k=find1(query[i].x);
				//根节点能量较小
				if(star[k].power>star[query[i].x].power)
					ans[sum++]=star[k].id;
				else
					ans[sum++]=-1;
			}
			//如果边被毁坏,则毁坏之前肯定联通,就加进去
		else
			merge1(query[i].x,query[i].y);
		if(flag)
			flag=0;
		else
			printf("\n");
		//逆向输出
		for(int i=sum-1;i>=0;i--)
			printf("%d\n", ans[i]);
	}
	return 0;
}

L:HDU-1272 小希的迷宫:判断是否存在树,一个图若是树需满足两个条件:连通分量为一;图中无环,包括自环和非自环。剩下的就是并查集的应用问题了,代码:

#include<iostream>
#include<cstdio>
#include<cstring>
const int MAX=100001;
int father[MAX];
int mark[MAX];
int n;
using namespace std;
void init()//初始化
{
    int i;
    for(i=1;i<=MAX;++i)
    father[i]=i,mark[i]=0;
}
int find_father(int x)
{
    if(x!=father[x])
        father[x]=find_father(father[x]);
    return father[x];
}
void join_tree(int x,int y)
{
    mark[x]=1;//只要加进来的都标记
    mark[y]=1;
    int a,b;
    a=find_father(x);
    b=find_father(y);
    if(a!=b)
    father[a]=b;
}
int main()
{
    int i,x,y,flag,sum;
    while(cin>>x>>y&&(x!=-1&&y!=-1))
    {
        init();
        memset(mark,0,sizeof(mark));
        if(find_father(x)==find_father(y))
        {
            if(x==0&&y==0)//特殊测试数据,0 0是符合的所以输出Yes
            cout<<"Yes"<<endl;
            else
            cout<<"No"<<endl;
        }
        else
        {
            flag=1;
            join_tree(x,y);//新节点加进树里面
            while(cin>>x>>y&&(x||y))
            {
                if(find_father(x)==find_father(y))//如果有环的话,就会相等,因为没环会逐渐加进树里面
                flag=0;
                else
                join_tree(x,y);
            }
            sum=0;
            for(i=1;i<MAX;++i)
            {
                if(father[i]==i&&mark[i])//一棵树只有一个根节点,如果多了根节点那么就不是树,也不符合题意所有的通道都是连通的
                sum+=1;
            }
            if(sum>1)//根节点不是1就输出No
            flag=0;
            if(flag==1)
            cout<<"Yes"<<endl;
            else
            cout<<"No"<<endl;
        }
    }
    return 0;
}

M:POJ-1308 Is It A Tree?:和上一题一样,只不过无向图换成了有向图:

#include<iostream>
#include<cstdio>
#include<cstring>
const int MAX=100001;
int father[MAX];
int mark[MAX];
int n,flag;
using namespace std;
void init()//初始化
{
    int i;
    for(i=1;i<=MAX;++i)
    father[i]=i,mark[i]=0;
}
int find_father(int x)
{
    if(x!=father[x])
        father[x]=find_father(father[x]);
    return father[x];
}
void join_tree(int x,int y)
{
    mark[x]=1;//只要加进来的都标记
    mark[y]=1;
    int a,b;
    a=find_father(x);
    b=find_father(y);
    if(a==b)
        flag=0;
    else if(a!=b)
    father[b]=a;  //a要是b的祖先
}
int main()
{
    int i,x,y,sum,t=0;
    while(cin>>x>>y&&(x!=-1&&y!=-1))
    {
        init();
        memset(mark,0,sizeof(mark));
        if(x==0&&y==0)
        {
            printf("Case %d is a tree.\n",++t);
            continue;
        }
        else
        {

            flag=1;
            join_tree(x,y);//新节点加进树里面
            while(cin>>x>>y&&(x||y))
            {
                if(x==0&&y==0)
                break;
                if(find_father(x)==find_father(y)||find_father(y)!=y)//如果有环的话,就会相等,因为没环会逐渐加进树里面
                flag=0;
                else
                join_tree(x,y);
            }
            sum=0;
            for(i=1;i<MAX;++i)
            {
                if(father[i]==i&&mark[i])//一棵树只有一个根节点,如果多了根节点那么就不是树,也不符合题意所有的通道都是连通的
                sum+=1;
            }
            if(sum>1)//根节点不是1就输出No
            flag=0;
            if(flag==1)
            printf("Case %d is a tree.\n",++t);
            else
            printf("Case %d is not a tree.\n",++t);
        }
    }
    return 0;
}