并查集:学习总结

$ $学习总结:并查集

蒟蒻的第一篇博客,如有bug,请大佬提出,勿喷。

并查集:##

并查集虽说是集合,不过个人觉得类似树形结构,像森林,刚开始每一个节点是一个森林,不断把森林合并,形成树。

• 是一组不相交的集合。即集合之间没有公共元素,它们的交集是空集。
• 因此没有“求集合的交集”和“求集合的差集” 操作。
• 至于“求集合的并集”运算,则只需简单地将两个集合的元素合并在一起就行了,不用考虑剔除重复元素的问题。


上模板:

【 P3367 【模板】并查集】

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入输出格式

输入格式:

第一行包含两个整数N、M,表示共有N个元素和M个操作。

接下来M行,每行包含三个整数Zi、Xi、Yi

当Zi=1时,将Xi与Yi所在的集合合并

当Zi=2时,输出Xi与Yi是否在同一集合内,是的话输出Y;否则话输出N

输出格式:

如上,对于每一个Zi=2的操作,都有一行输出,每行包含一个大写字母,为Y或者N

代码:

#include<iostream>
using namespace std;
int father[100000+10];
void init(){						//初始化
    for(int i=0;i<10000+10;i++)  father[i]=i;	//爸爸就是自己
}
int find(int x){					//找祖先
    if(father[x]==x)  return x;		//如果爸爸是自己的话,那么他就是这整个集合的root,及祖先。
    else  return father[x]=find(father[x]);	//否则继续向上找
}
void findans(int x,int y){			//判两数是否是否在同一集合(并查集)
    int i=find(x);					//找祖先
    int j=find(y);					//同上
    if(i==j)  cout<<'Y'<<endl;		//如果祖先相同,及在同一并查集
    else
    cout<<'N'<<endl;
}
void union_(int x,int y){			//合并
    int i=find(x);
    int j=find(y);
    if(i!=j)  father[i]=j;			//将一个数的父亲指向另一个数
}
int main(){
    int n,m,p;
    cin>>n>>m;
    init();
    for(int i=1;i<=m;i++){
        int e,a,b;
        cin>>e>>a>>b;
        if(e==1)					//操作:合并
        union_(a,b);
        else						//操作:询问
        findans(a,b);
    }
    return 0;
}

带秩并查集:

什么是秩:秩就是并查集的节点数。

经典题:【打击犯罪】

某个地区有n(n≤1000)个犯罪团伙,当地警方按照他们的危险程度由高到低给他们编号为1-n,他们有些团伙之间有直接联系,但是任意两个团伙都可以通过直接或间接的方式联系,这样这里就形成了一个庞大的犯罪集团,犯罪集团的危险程度由集团内的犯罪团伙数量唯一确定,而与单个犯罪团伙的危险程度无关(该犯罪集团的危险程度为n)。现在当地警方希望花尽量少的时间(即打击掉尽量少的团伙),使得庞大的犯罪集团分离成若干个较小的集团,并且他们中最大的一个的危险程度不超过n/2。为达到最好的效果,他们将按顺序打击掉编号1到k的犯罪团伙,请编程求出k的最小值。

【输入】
第一行一个正整数n。接下来的n行每行有若干个正整数,第一个整数表示该行除第一个外还有多少个整数,若第i行存在正整数k,表示i,k两个团伙可以直接联系。

【输出】
一个正整数,为k的最小值。

【输入样例】
7
2 2 5
3 1 3 4
2 2 4
2 2 3
3 1 6 7
2 5 7
2 5 6

【输出样例】
1

【思路】:审题,题目要求,按顺序打击1~k的罪犯团伙,也就是说,要打k号犯罪团伙,必须先打掉1~k-1号犯罪团伙。所以,我们可以从后往前建并查集,一旦发现并查集的秩超过了题目要求(n/2)那么直接输出结果(当前操作的编号)。

【代码】

#include<iostream>
#include<cmath>
#include<cstdlib>
#include<cstdio>
using namespace std;
int father[100000+10],num[100000+10],dis[100000+10],ans,k,o,e[1000001],a[1001][1001];
void init()		//初始化
{
    for(int i=0;i<50000+10;i++){father[i]=i;num[i]=1;}
}
int find(int x)	//找祖先
{
    if(x!=father[x])
    {
        father[x]=find(father[x]);
    }
    return father[x];
}
void merge(int x,int y)
{
    int a1=find(x),a2=find(y);
    if(a1!=a2)
    {
        father[a1]=a2;
        num[a2]+=num[a1];		//秩累加
        if(num[a2]>k){cout<<o<<endl;exit(0);}	//判断是否满足题意
        //if(num[a2]>=k){cout<<o<<endl;exit(0);}
//        num[a1]=num[a2];
    }
}
int main()
{
    int n,m,p;
    cin>>m;
    k=m/2;
    init();
    for(int i=1;i<=m;i++)
    {
        cin>>e[i];
        for(int j=1;j<=e[i];j++)
        {
        	cin>>a[i][j];
       	}
    }
    for(int i=m;i>=1;i--)
    {
    	o=i;
    	for(int j=1;j<=e[i];j++)
    	{
    		if(i<a[i][j])	//如果(i>a[i][j])  a[i][j]已经被打掉了,所以没用
    		merge(i,a[i][j]);	//建立并查集
    	}
    }
    return 0;
}

带权并查集&&n倍空间

什么是权:权就是权值……

经典题:【食物链】&&【银河英雄传说】

P1196 [NOI2002]银河英雄传说

题目描述

公元五八○一年,地球居民迁移至金牛座α第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。

宇宙历七九九年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。

杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成30000列,每列依次编号为1, 2, …, 30000。之后,他把自己的战舰也依次编号为1, 2, …, 30000,让第i号战舰处于第i列(i = 1, 2, …, 30000),形成“一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为M i j,含义为让第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。

然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。

在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。

作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。

最终的决战已经展开,银河的历史又翻过了一页……

【样例输入】 
4 
M 2 3 
C 1 2 
M 2 4 
C 4 2 

【样例输出】 
-1 
1

说明

【样例说明】

战舰位置图:表格中阿拉伯数字表示战舰编号

【思路】

在建并查集的过程中储存两战舰的距离,找答案的时候加以处理。

【代码】

#include<iostream>
#include<cmath>
#include<cstdio>
using namespace std;
int father[100000+10],num[100000+10],dis[100000+10],ans;
void init()			//初始化
{
    for(int i=0;i<50000+10;i++){father[i]=i;num[i]=1;}
}
int find(int x)		//找祖先
{
    if(x!=father[x])
    {
        int k=father[x];
        father[x]=find(father[x]);
        dis[x]+=dis[k];	//累加两战舰的距离
        num[x]=num[father[x]];	//加权
    }
    return father[x];
}
int findans(int a,int b)		//找答案
{
    int r1=find(a),r2=find(b);
    if(r1!=r2)
    {
    	return -1;
    }
    else{
        return abs(dis[a]-dis[b])-1;
    }
}
void merge(int x,int y)	//模板
{
    int a1=find(x),a2=find(y);
    if(a1!=a2)
    {
        father[a1]=a2;
        dis[a1]=dis[a2]+num[a2];
        num[a2]+=num[a1];	//权
        num[a1]=num[a2];
    }
}
int main(){
    int n,m,p;
    cin>>m;
    init();
    for(int i=1;i<=m;i++)
    {
        int a,b;
        char e;
        cin>>e>>a>>b;
        if(e=='M')
        merge(a,b);	//建并查集
        else
        {
            ans=findans(a,b);
            printf("%d
",ans);
        }
    }
    return 0;
}

n倍空间并查集

【团伙】此题是双倍空间,【食物链】是三倍空间,当然【食物链】也可以用带权并查集来做。

【食物链】

描述

动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。

现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这N个动物所构成的食物链关系进行描述:

第一种说法是"1 X Y",表示X和Y是同类。

第二种说法是"2 X Y",表示X吃Y。

此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

1) 当前的话与前面的某些真的话冲突,就是假话;

2) 当前的话中X或Y比N大,就是假话;

3) 当前的话表示X吃X,就是假话。

你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。

输入

第一行是两个整数N和K,以一个空格分隔。

以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。

若D=1,则表示X和Y是同类。

若D=2,则表示X吃Y。

输出

只有一个整数,表示假话的数目。

样例输入 
100 7 
1 101 1 
2 1 2 
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5 
样例输出 
3

【思路】
开三倍空间,指的是把数组开三倍空间,分别存同类,猎物,天敌。然后把每一次输入的数据进行判断,如果发现不符题意,那么就是谎话,否则把所有有关联的同类连接在一起。

【代码】

#include<iostream>
#include<string.h>
#include<cstdio>
#include<cstdlib>
using namespace std;
long long father[150000+10],t[1000001],ans,o,x,y,k,vis[100001],a,b,c;
void init(int n){			//三倍空间初始化
    for(int i=1;i<=n;i++)
    {
        father[i]=i;
        father[i+n]=i+n;
        father[i+n+n]=i+n+n;
    }
}
int find(int x)			//找祖先
{
    if(father[x]==x)
{
        return x;
    }
    else  
    {return father[x]=find(father[x]);}
}
void union_(int x,int y){		//模板
    int i=find(x);
    int j=find(y);
    if(i!=j){father[i]=j;}
}
int main()
{
    int n,m,p;
    cin>>n>>m;
    init(n);
    for(int i=1;i<=m;i++)
    {
        cin>>c>>a>>b;
        if(a>n||b>n)		//判断符不符题意
        {
            ans++;
            continue;
        }
        if(c==1)			//同类情况
        {
            if(find(a+n)==find(b)||find(a+n+n)==find(b))	//判断符不符题意
            ans++;
            else
            {
                union_(a,b);				//同类处理
                union_(a+n,b+n);
                union_(a+n+n,b+n+n);
            }
        }
        else
        {
            if(a==b)				//判断符不符题意
            {
                ans++;
                continue;
            }
            if(find(a)==find(b)||find(a+n+n)==find(b))
            ans++;
            else
            {
                union_(a,b+n+n);			//非同类处理
                union_(b,a+n);
                union_(a+n+n,b+n);
            }
        }
    }
    cout<<ans<<endl;				//输出
    return 0;
}

总结:

1、并查集要注意关系,不能漏建,不能重建,不然会WA。

2、Find(),merge()的模板要记住,一般进行修改都会在这两个函数里改。

3、合并的时候有时要注意方向,不要搞反了。

随便给点题:

UVA1316 Supermarket

UVA615 Is It A Tree?

UVA1197 The Suspects

P1195 口袋的天空

P1525 关押罪犯

POJ 1988 Parity game
(题解链接:大佬博客

$ $ 谢谢观赏,给个赞呗qwq

原文地址:https://www.cnblogs.com/hyfhaha/p/10678190.html