并查集全面讲解

\[by~~~StarMaster\\update~~~in~~~2021.8.8 \]

并查集

1.用途:

并查集是一种树形的数据结构,处理无交集的合并和查询问题.每个集合有一个代表元素,通过代表元素进行便捷的合并和查询问题。

2.两种基本操作:

查找(Find )和合并(merge )

//fa[i]=i;
int Find(int x)
{
    if(x!=fa[x]) return Find(fa[x]);
    else return fa[x];
}
void merge(int x,int y)
{
    fx=Find(x),fy=Find(y);
    if(fx!=fy) fa[fx]=fy;
}

基本操作的优化

当一个集合中元素很多时,后代会越来越多每次的查询就会跨越很多儿子找到祖宗,是十分低效的。

所以要通过一些操作来实现高效的查询,也可使合并变得迅速

(1)路径压缩

每次查找都是找祖先,那么与其从下往上一代一代的查父亲不如每次查询后都将后代接到祖宗上(父亲变兄弟),这样每次查询都会极快

int Find(int x)
{
    if(x==fa[x]) return x;
    else 
    {
        fa[x]=Find(fa[x]);//将后代接到祖宗上;
        return fa[x];
    }
}

(2)按秩合并(启发式合并)

将元素少的集合并到元素大的集合上。

由于需要我们支持的只有集合的合并、查询操作,当我们需要将两个集合合二为一时,无论将哪一个集合连接到另一个集合的下面,都能得到正确的结果。但不同的连接方法存在时间复杂度的差异。具体来说,如果我们将一棵点数与深度都较小的集合树连接到一棵更大的集合树下,显然相比于另一种连接方案,接下来执行查找操作的用时更小

//sz[i]=1;
void merge(int x,int y)
{
    int fx=Find(x),fy=Find(y);
    if(fx==fy) return ;
    if(sz[fx]>sz[fy]) 
    {
        fa[fy]=fx;
        sz[fx]+=sz[fy];
    }
    else
    {
        fa[fx]=fy;
        sz[fy]+=sz[fx];
    }
}

3.例题

1.P1551 亲戚

题目大意

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
(规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚)

输入格式

第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

分析:

显而易见这是一道裸的并查集板子题,将是亲戚的并到一起,因为无需考虑任何的家族关系\(eg\):谁是谁的父亲爷爷等),所以可以直接进行路径压缩(无需任何冗杂操作)。

代码:

#include<bits/stdc++.h>
#define ovo getchar()
#define endl '\n'

using namespace std;
inline int read()
{
	int  x=0,f=1;
	char ch;
	ch=ovo;
	while(ch>'9' or ch<'0')
	{
		if(ch=='-') f=-1;
		ch=ovo;
	}
	while(ch>='0' and ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^48);
		ch=ovo;
	}
	return x*f;
}

int n,m,fa[2004110],p;

int Find(int x)
{
	if(fa[x]==x) return x;
	else return fa[x]=Find(fa[x]);
}
int ans=-1;
inline int StarMaster()
{
	n=read();
	m=read();
    p=read();
    for(int i=1;i<=n;i++)
    {
    	fa[i]=i;
    }
    for(int i=1;i<=m;i++)
    {
    	int x=read(),y=read();
    	fa[Find(x)]=Find(y);
    }
    for(int i=1;i<=p;i++)
    {
    	int x=read(),y=read();
    	if(Find(x)==Find(y)) cout<<"Yes"<<'\n';
    	else cout<<"No"<<'\n';
    }
	return (0x0);
}
int true_answer=StarMaster();
signed main(){;}
/*
10 11
5 3 39
9 3 71
7 6 36
7 3 23
6 1 69
4 7 33
10 4 92
9 6 54
7 9 80
10 5 3
6 5 6
*/

2.P2078 朋友

题目背景

小明在 A 公司工作,小红在 B 公司工作。

题目描述

这两个公司的员工有一个特点:一个公司的员工都是同性。

A 公司有 \(N\) 名员工,其中有 \(P\) 对朋友关系。\(B\) 公司有 \(M\) 名员工,其中有 \(Q\) 对朋友关系。朋友的朋友一定还是朋友。

每对朋友关系用两个整数 \((X_i,Y_i)\) 组成,表示朋友的编号分别为 \(X_i,Y_i\)。男人的编号是正数,女人的编号是负数。小明的编号是 \(1\),小红的编号是 \(−1\)

大家都知道,小明和小红是朋友,那么,请你写一个程序求出两公司之间,通过小明和小红认识的人最多一共能配成多少对情侣(包括他们自己)。

输入格式

输入的第一行,包含 \(4\) 个空格隔开的正整数 \(N,M,P,Q\)

之后 \(P\) 行,每行两个正整数 \(X_i,Y_i\)

之后 \(Q\) 行,每行两个负整数 \(X_i,Y_i\)

输出格式

输出一行一个正整数,表示通过小明和小红认识的人最多一共能配成多少对情侣(包括他们自己)。

(摘自洛谷)

分析:

首先可以明显看出这是并查集(维护关系)

我们可以将男女分开考虑,先将这几对关系处理合并,然后暴力求出与小明有关系的人数,和与小红有关系的人数,

再取min,出最大配对数。

答案就出来了

代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
	int  x=0,f=1;
	char ch;
	ch=getchar();
	while(ch>'9' or ch<'0')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' and ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}
const int MMM=20990;
int n,m,p,q,fa_men[MMM],fa_women[MMM],men,women,ans;
inline int Min(int a,int b) {return a<b?a:b;}
inline int Find_men(int x)
{
	if(fa_men[x]==x) return x;
	return fa_men[x]=Find_men(fa_men[x]);
}
inline int Find_women(int x)
{
	if(fa_women[x]==x) return x;
	return fa_women[x]=Find_women(fa_women[x]);
}
int main()
{
	n=read(),m=read();
	p=read(),q=read();
	for(int i=1;i<=n;i++)
	{
		fa_men[i]=i;//男性
	}
	for(int i=1;i<=m;i++)
	{
		fa_women[i]=i;//女性
	}
	for(int i=1;i<=p;i++)
	{
		int x=read(),y=read();
		fa_men[Find_men(x)]=Find_men(y);//男性
	}
	for(int i=1;i<=q;i++)
	{
		int x=-read(),y=-read();//处理负值
		fa_women[Find_women(x)]=Find_women(y);//女性
	}
	for(int i=1;i<=n;i++)
	{
		if(Find_men(i)==Find_men(1))//暴力判断男性中认识小明的人数
		{
			men++;
		}
	}
	for(int i=1;i<=m;i++)
	{
		if(Find_women(i)==Find_women(1))//暴力判断女性中认识小红的人数
		{
			women++;
		}
	}
	ans=Min(men,women);//要保证配对所以men(男性中认识小明的人数)和women(女性中认识小红的人数)取min;
	cout<<ans;
	return (0x0);
}

3.洛谷P1197星球大战

题目大意:

n个点(0-n-1编号)m条无向边,k次删点(同时将它的边删除),问每次删完后联通快的个数。

输入:

输入\(n(1<=n<=4e5),m(1<=m<=2e5)\)
接下来m行,每行包括x,y表示x,y间有边相连
输入k;
紧跟着k行为k次删点。

输出:

第一行为开始时联通快的个数;
接下来k行为每次删点后联通快的个数。

分析:

如果正序做,每次删点后重新建图,算出联通快个数,显然时间上根本过不去。所以脑子急转180,我们采取倒序做的方法如同防线修建一样,先将k次删点离线下来,然后倒序加点,在用并查集判联通即可。

代码:

#include<bits/stdc++.h>

#define endl '\n'

using namespace std;
inline int read()
{
	int  x=0,f=1;
	char ch;
	ch=getchar();
	while(ch>'9' or ch<'0')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0' and ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*f;
}

const int MM=1e6;

int n,m,fa[MM],p,k;

int Find(int x)
{
	if(fa[x]==x) return x;
	else return fa[x]=Find(fa[x]);
}

inline void merge(int x,int y)
{
	int fx=Find(x),fy=Find(y);
	if(fx!=fy)
	{
		fa[fx]=fy;
	}
}
int head[MM],nxt[MM],ver[MM],tot,pre[MM];
inline void add(int x,int y)
{
	ver[++tot]=y;
	pre[tot]=x;
	nxt[tot]=head[x];
	head[x]=tot;
}

int ans[MM],a[MM];
bool vis[MM];

inline int StarMaster()
{
	n=read();
	m=read();
    for(int i=0;i<n;i++)
    {
    	fa[i]=i;
    }
    for(int i=1;i<=m;i++)
    {
    	int u=read(),v=read();
    	add(u,v);
        add(v,u);//无向边
    }
    k=read();
    for(int i=1;i<=k;i++)
    {
    	a[i]=read();
    	vis[a[i]]=1;//标记已删
    }
    int cnt = n-k;
    for(int i=1;i<=2*m;i++)//2*m条边均枚举一遍
    {
    	int fx=Find(pre[i]),fy=Find(ver[i]);
    	if(!vis[pre[i]] and !vis[ver[i]] and fx!=fy )//边起点和终点均未被删除且二者未在同一联通快,则
    	{
    		cnt--;//;联通块数-1
    		merge(pre[i],ver[i]);//将两点合并为同一联通块
    	}
    }
    ans[k]=cnt;//k次删点后的联通块数即为当前cnt
    for(int i=k;i>=1;i--)
    {
    	cnt++;//初始加点后,联通快数+1
    	vis[a[i]]=0;//恢复

    	for(int j=head[a[i]] ; j ; j=nxt[j])
    	{
    		if(!vis[ver[j]] and Find(a[i])!=Find(ver[j]))
    		{
    			cnt--;//有边相连且未被摧毁联通快数减一,再将其合并成一个联通块
    			merge(a[i],ver[j]);
    		}
    	}
    	ans[i-1]=cnt;//第i次删点恢复之后是i-1次摧毁完之后的联通快数量
    }
    for(int i=0;i<=k;i++)
    cout<<ans[i]<<endl;
	return (0x0);
}
int true_answer=StarMaster();
signed main(){;}

4.P3465[POI2008]CLO-Toll

题目大意:

给你 \(n\) 个点和 \(m\) 条双向边,问能否将其中的一些边改成有向边,使得只考虑有向边的情况下每个点的入度都为 \(1\)

输入格式:

第一行输入 \(n,m(1≤n≤100000,1≤m≤200000)\),接下来 \(m\) 行每行两个数 \(a,b\) 表示点 \(a\) 和点 \(b\) 之间有一条双向边。输入保证没有重边与自环。

输出格式:

若没有合法方案,输出 \(NIE\) ,否则先在第一行输出 \(TAK\) ,然后在第 \(i+1\) 行输出点 \(i\) 的入度是由哪个点出发的边所得到的。

分析:

从m条双向边中选取一些边,选定方向后使每个点入度为1

那么问题来了,什么样的图中所有点入度都为1呢?

存在环的树(基环树)(因为会产生 \(n\) 个节点和 \(n\) 条边在同一棵树上的情况);


代码:

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int x=0,f=1;
    char ch=getchar();
    while(ch>'9' or ch<'0')
    {
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' and ch<='9')
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();    
    }
    return f*x; 
}
const int MM=1e6+55;
int n,m,tot,fa[MM];
int head[MM],nxt[MM],ver[MM],ans[MM];
struct node
{
    int x;
    int y;
    bool f;
}e[MM];
inline int Find(int x)
{
    if(x==fa[x]) return x;
    else return fa[x]=Find(fa[x]);
}
inline void merge(int x,int y)
{
    int fx=Find(x),fy=Find(y);
    fa[fx]=fy;
    return ;
}
inline void add(int x,int y)
{
    ver[++tot]=y;
    nxt[tot]=head[x];
    head[x]=tot;
}//散装数组建图 
inline void dfs(int x,int y)
{
    ans[x]=y;
    for(int i=head[x];i;i=nxt[i])
    {
        int v=ver[i];
        if(!ans[v])
        {
            dfs(v,x);
        }
    }
}//寻找路径 
inline int StarMaster()
{
    n=read(),m=read();
    for(int i=1;i<=n;i++)
    {
        fa[i]=i;
    }
    for(int i=1;i<=m;i++)
    {
        e[i].x=read(),e[i].y=read();
        if(Find(e[i].x)==Find(e[i].y)) continue;
        add(e[i].x,e[i].y);add(e[i].y,e[i].x);
        merge(e[i].x,e[i].y);
        e[i].f=true;
    }
//建立初始树(无环),之后遍历剩余的边,是否能出现环 
    for(int i=1;i<=m;i++)
    {
        if(e[i].f) continue;
        if(ans[e[i].x]) continue;//如果已出现在路径里则不进行DFS 
        dfs(e[i].x,e[i].y); 
    }
//dfs即为找环的过程 
    for(int i=1;i<=n;i++)
    {
        if(!ans[i])
        {
            cout<<"NIE"<<'\n';
            return 0;
        }//但凡有一个点没有被遍历到,则说明无环即无解 
    }
    cout<<"TAK"<<'\n';
    for(int i=1;i<=n;i++)
    {
        cout<<ans[i]<<'\n';
    }
    return 0;
}

int Star_Master=StarMaster();
int main(){;}

拓展:

带权并查集

内容:

我们可以在并查集的边上定义某种权值、以及这种权值在路径压缩时产生的运算,从而解决更多的问题

操作

用父亲节点记录子树的权值,显而易见根节点就记录树的权值

  inline int Find(int x)
{
	if(x==fa[x]) return x;
	int fx=fa[x];
	fa[x]=Find(fx);
	dis[x]+=dis[fx];
	return fa[x];
}//dis[x]为x节点到根节点距离 

void merge(int x,int y,int s)
{
    fx=Find(x),fy=Find(y);
    if(fx!=fy) 
	{
		fa[fx]=fy;
		dis[fx]=dis[y]-dis[x]+s;
	}
}
(每个题的更新dis的方法会有所不同,视题意而定)

例题

带权并查集例题真的难找 \(qwq\)

1.洛谷P1196 [NOI2002] 银河英雄传说

题目描述

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

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

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

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

输入格式

第一行有一个整数T(1<=T<=5e5),表示有T条指令。
接下来T行,每行一条指令,两种
1.M i j (i,j为正整数且<=30000) 表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第i号战舰与第j号战舰不在同一列
2.C i j(i,j为正整数且<=30000)表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令.

输出格式

依次对输入的每一条指令进行分析和处理:
如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息。
如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第i号战舰与第 j号战舰之间布置的战舰数目。如果第 i号战舰与第 j 号战舰当前不在同一列上,则输出 −1。

分析:

合并操作,直接进行并查集合并即可,但询问操作 显然 用普通并查集无法实现 , 所以我们采用带权并查集。每个点不仅记录他的祖宗还要记录他的深度,然后进行路径压缩

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>

using namespace std ;

struct StarMaster
{
	private :
		int n , m , deep[300010] , fa[300010] , sum[300010] ;
		
		inline int read()
		{
			int x = 0 , f = 1 ;
			char ch = getchar() ;
			while(ch > '9' or ch < '0')
			{
				if(ch == '-') f = -1 ;
				ch = getchar() ;
			} 
			while(ch >= '0' and ch <= '9')
			{
				x = (x << 1) + (x << 3) + (ch^48) ;
				ch = getchar() ;
			}
			return x * f ;
		}
		inline void Init()
		{
			for(int i = 1 ; i <= 30000 ; i ++)
			{
				fa[i] = i ;
				sum[i] = 1 ;
			}
		}
		inline int Find(int x)
		{
			if(x == fa[x]) return x ;//递归边界 
			else
			{
				int tmp = fa[x] ;//记录下来 fa[x] ,  便于递归 
				fa[x] = Find(fa[x]) ;//进行下推 保证 deep[fa[x]] 已被更新  
				deep[x] += deep[tmp] ;//更新 x 的深度将 x 所在队列根节点 fx 接到 y 的后面 , 所以 x 的深度为他在原队列的深度加上他父亲结点的深度(相当于一个递归的过程从 x 推到相接的 fx 再回推更新  deep[fx]' = deep[fx] + deep[y] ,推到对列的末端 
				return fa[x] ;
			}
		}
		inline int abs(int x)
		{
			return x > 0 ? x : -x ;
		}
	public :
		inline void ioi()
		{
		    m = read() ;
			Init();
			for(int i = 1 ; i <= m ;i ++)
			{
				char opt ;
				cin >> opt ;
				if(opt == 'M')
				{
					int x = read() , y = read();
					int fx = Find(x) , fy = Find(y) ;
					fa[fx] = fy ;
					deep[fx] = sum[fy] ;
				    sum[fy] += sum[fx] ;
				} //sum用于计算各点所在队列末端到对首的总深度。 
				if(opt == 'C')
				{
					int x = read() , y = read() ;
					int fx = Find(x) , fy = Find(y) ;
					if(fx == fy) 
					{
						cout << abs(deep[x] - deep[y]) - 1 <<'\n';
					}
					else
					{
						cout << -1 <<'\n' ;
					}
				} 
			}
		}
}ak; 

int main()
{
    ak.ioi() ;	
	return 0 ;
}

2.P2024 [NOI2001] 食物链

题目描述

动物王国中有三类动物 $ 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\) 句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

  • 当前的话与前面的某些真的话冲突,就是假话
  • 当前的话中 \(X\)\(Y\)\(N\) 大,就是假话
  • 当前的话表示 \(X\)\(X\),就是假话

你的任务是根据给定的 \(N\)\(K\) 句话,输出假话的总数。

输入格式

第一行两个整数,\(N\)\(K\),表示有 \(N\) 个动物,\(K\) 句话。

第二行开始每行一句话(按照题目要求,见样例)

输出格式

一行,一个整数,表示假话的总数。

分析 :

两种方法:
方法一

先说一个巧妙的方法 (大部分人最先想到的方法):

fa[] 开三倍 用于 分别存储三种关系 来进行操作 。

\(1···n\) 存储同类 , \(n+1 ··· 2n\) 存储猎物 ,\(2n + 1···3n\) 存储猎杀者

代码:
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>

using namespace std ;

struct StarMaster
{
	private :
		int n , m , fa[200010] , ans = 0;
		
		inline int read()
		{
			int x = 0 , f = 1 ;
			char ch = getchar() ;
			while(ch > '9' or ch < '0')
			{
				if(ch == '-') f = -1 ;
				ch = getchar() ;
			} 
			while(ch >= '0' and ch <= '9')
			{
				x = (x << 1) + (x << 3) + (ch^48) ;
				ch = getchar() ;
			}
			return x * f ;
		}
		
		inline int Find(int x)
		{
			if(fa[x] == x) return x ;
			return fa[x] = Find(fa[x]) ;
		}
		
		inline void Init()
		{
			for(int i = 1 ; i <= 3 * n ; i ++)
			{
				fa[i] = i ;
			}
		}
		
		inline int abs(int x)
		{
			return x > 0 ? x : -x ;
		}
	
	public :
		inline void ioi()
		{
		    n = read() , m = read() ;//1 ~ n 存储同类 , n+1 ~ 2n 存储猎物 ,2n+1 ~ 3n 存储猎杀者  
		    Init() ;
			for(int i = 1 ; i <= m ; i ++)
		    {
		    	int opt = read() , x = read() , y = read() ;
		    	if( x > n or y > n) 
		    	{
		    		ans ++ ;
		    		continue ;
		    	}//最离谱的假话直接记录;
				if(opt == 1)
				{
					if(Find(x + n) == Find(y) or Find(x + 2 * n) == Find(y))
					{
						ans ++ ;
						continue ;
					} //如果 y 是 x 的猎物或猎杀者 , 那么显然 x 和 y 必然不是同类 
					else
					{ 
					    fa[Find(x)] = Find(y) ;
						fa[Find(x + n)] = Find(y + n) ;
						fa[Find(x + n * 2)] = Find(y + n * 2) ;	
					} // 同类所有都一样 
				} 
				else  
				{
					if ( x == y or Find(x + 2 * n) == Find(y) or Find(x) == Find(y))
					{
						ans ++ ;
						continue ;
					}
					else
					{
						fa[Find(x)] = Find(y + 2 * n) ;// x 与 y + 2n 并 说明 x 是 y 的 捕杀者 
						fa[Find(x + n)] = Find(y) ;//x + n 与 y 并表示 y 是 x 的猎物 
						fa[Find(x + 2 * n)] = Find(y + n) ;//x + 2n 与 y + n 并表示 y 的猎物 是 x 的捕杀者(题目中已说明总共 3 种动物捕食关系形成一个环) 
				    }
				} 
		    }
		    cout  << ans << '\n' ; 
		}
}ak; 

int main()
{
    ak.ioi() ;	
	return 0 ;
}
方法二

第二种方法(真 · 解):用权值表示关系 。

据题意,森林中有 $ 3 $ 种动物:\(A\)\(B\)\(B\)\(C\)\(C\)\(A\)

使用带权并查集,我们就以动物之间的关系来作为并查集每个结点的权值。

注意,我们不知道所给的动物(题目说了,输入只给编号)所属的种类。所以,我们可以用动物之间的“相对关系”来确定一个并查集

我们用 $ rs[] $ 表示关系(\(relationship\)\(rs[x]=0\) 表示 这个节点 \(x\)\(fa[x]\) 是同类 , \(rs[x]=1\) 表示 \(x\)\(fa[x]\) 的猎物, \(rs[x]=2\) 表示 \(x\)\(fa[x]\) 的捕杀者,每次合并后对 \(3\) 进行取模 。(初始 $ rs[x] = 0 $ 自己和自己是同类 ) 。

推算 :

x 吃 y 共根 表示 :

(1) \(rs[x] = 2\) , \(rs[y] = 0\) (根节点是 \(x\) 的猎物 , \(y\) 与根节点是同类,所以 \(x\)\(y\)

(2) \(rs[x] = 1\) , \(rs[x] = 2\) ( 根节点是 \(x\) 的捕杀者 ,根节点是 \(y\) 的猎物 , 所以 \(x\)\(y\) ) ;

(3) \(rs[x] = 0\) , \(rs[y] = 1\) (根节点和 \(x\) 是同类 , 根节点是 \(y\) 的捕杀者 , 所以 \(x\)\(y\));

共上述三种情况 , 归纳 : $ x $ 吃 y 可表示为 $ rs[y] = (rs[x] + 1)%3 $


我们可以用向量加减法推导更新权值的通式 :

\[rs[fa[x]] = 0 \\ rs[x] = 1\\ rs`[x] = 1\\ rs`[x] = (rs[fa[x]] + rs[x])~\%~3\\ ***\\ rs[fa[x]] = 1 \\ rs[x] = 1 \\ rs`[x] = 2\\ rs`[x] = (rs[fa[x]] + rs[x])~\%~3\\ \dots \]

经过推算我们发现 \(rs[x]\) 的合并(路径压缩)符合向量加减法运算


下面我们来推算两个不同根的合并

假设我们要合并 \(x\)\(y\)

我们有输入的 \(d\) ( \(x\)\(y\) 的关系) , 转化为我们所用为 \(d - 1\) 所以由向量加减法得

\[rs`[y] = (d-1 + rs[x])~\%~3\\ rs`[fa[y]] = (ra`[y]-rs[y] + 3)~\%~3\\ \]

检验

\[rs[x]=0\\ d-1=2\\ rs`[y]=2\\ rs[y]=1\\ rs`[fa[y]]=1\\ **\\ rs`[fa[y]]=(rs`[y]-rs[y]+3)~\%~3 \]

检验成立

判断合不合法 :

我们首先观察条件2和条件3 :

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

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

十分智障 直接简单的 \(if\) 解决

再看条件1

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

我们已推出关系转化符合向量加减法 , 所以我们只需判断在同根的情况下此条件能否满足向量加减法即可( 因为不同根情况下必不会产生冲突 )

\[because :\\ ~~~rs[y]=(d-1+rs[x])~\%~3\\ so :\\ ~~~d-1=(rs[y]-rs[x]+3)~\%~3 \]

代码:
#include<bits/stdc++.h>

using namespace std ;

class StarMaster
{
    private :
        int n , k , ans , fa[100010] , rs[100010] ;
    	inline int read()
        {
            int x = 0 , f = 1 ;
            char ch = getchar() ;
            while(ch < '0' or ch > '9')
            {
                if(ch == '-') f = -1 ;
                ch = getchar();
            }
            while(ch >= '0' and ch <= '9')
            {
                x = (x << 1) + (x << 3) + (ch ^ 48) ;
                ch = getchar() ;
            }
            return x * f ;
	    }
        inline int Find(int x)
        {
            if(fa[x] == x) return x ;
            else
            {
                int tmp = fa[x] ;
             	fa[x] = Find(fa[x]) ;
                rs[x] = (rs[tmp] + rs[x]) % 3 ;
                return fa[x] ;
		    }
        }
        inline void merge(int x , int y , int d)
        {
            int fx = Find(x) , fy = Find(y) ;
            if(fx == fy) return ;
            else
            {
                fa[fy] = fx ;
                rs[fy] = (d - 1 + rs[x] - rs[y] + 3) % 3 ;   
			}
            return ;
        }
        inline void Init()
        {
        	for(int i = 1 ; i <= n ; i ++)
            {
                fa[i] = i ;
                rs[i] = 0 ;
            }
        }
    public :
        inline void ioi()
        {
            n = read() , k = read() ;
            Init() ;
            for(int i = 1 ; i <= k ; i ++)
            {
                int d = read() , x = read() , y = read() ;
                if((x > n or y > n) or (d == 2 and x == y) )
                {
                    ans ++ ;
                    continue ;
                }
                int fx = Find(x) , fy = Find(y) ;
                if(fx == fy)
                {
                    if((d - 1)!=(rs[y] - rs[x] + 3) % 3)
                    {
                        ans ++ ;
                        continue ;
                    }
                }
                else
                {
                    merge(x , y , d) ;
                }
            }
            cout << ans << '\n' ;
        }
}ak;

int main()
{
	ak.ioi() ;
	return 0 ;
}

种类并查集

内容:

一般的并查集,维护的是具有连通性、传递性的关系,例如亲戚的亲戚是亲戚。但是,有时候,我们要维护另一种关系:敌人的敌人是朋友。种类并查集就是为了解决这个问题而诞生的。满足这种关系就可以用带权加取模运算解决。(目前均是如此)

操作 :

目前有两种主流 :

一个 将\(fa[x]\) 数组呈倍数至所需 并加入一些判断

第二个是 利用带权并查集的思想 , 进行种类划分与判断

其实就是带权并查集

例题:

1.P2024 [NOI2001] 食物链

详解同上

只是另一种理解

2.P1525[NOIP2010 提高组] 关押罪犯

题目描述

S 城现有两座监狱,一共关押着 \(N\) 名罪犯,编号分别为 \(1-N\)。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为 \(c\) 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为 \(c\) 的冲突事件。

每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到 \(S\)\(Z\) 市长那里。公务繁忙的 \(Z\) 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。

在详细考察了\(N\) 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。

那么,应如何分配罪犯,才能使 Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?

输入格式

每行中两个数之间用一个空格隔开。第一行为两个正整数$ N,M$,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的 \(M\) 行每行为三个正整数 \(a_j,b_j,c_j\),表示 \(a_j\) 号和 \(b_j\) 号罪犯之间存在仇恨,其怨气值为 \(c_j\)。数据保证$ 1<aj≤bj≤N,0<cj≤1091<a_j\leq b_j\leq N$ ,$ 0 < c_j\leq 10^91<aj≤bj≤N,0<cj≤109$,且每对罪犯组合只出现一次。

输出格式

\(1\) 行,为 \(Z\) 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出 0

分析 :

很明显这道题是一道种类并查集 , 敌人的敌人是朋友,朋友的朋友是朋友,敌人的朋友是敌人。

\(fa[x]\) 数组开两倍,\(1-n\) 存储第一座监狱 , \(n + 1 —— 2 * n\) 存储第二座监狱。

代码:

#include<bits/stdc++.h>

using namespace std ;

int n , m ;
const int MM = 1e6 + 66666 ;
int fa[MM << 1] ;
inline int read()
{
	int x = 0 , f = 1 ;
	char ch = getchar() ;
	while(ch < '0' or ch > '9')
	{
		if(ch == '-') f = -1 ;
		ch = getchar() ;
	}
	while(ch >= '0' and ch <= '9')
	{
		x = (x << 1) + (x << 3) + (ch ^ 48) ; 
		ch = getchar() ;
	}
	return x * f ;
}

struct node
{
	int x , y , z ;
}a[MM];

inline int Find(int x)
{
	if(fa[x] == x) return x ;
	return fa[x] = Find(fa[x]) ;
}

inline bool cmp(node a , node b)
{
	return a.z > b.z ;
}

inline int StarMaster()
{
	n = read() , m = read() ;
	for(int i = 1 ; i <= n ; i ++)
	{
		fa[i] = i ;
		fa[i + n] = i + n;
	}
	for(int i = 1 ; i <= m ; i ++)
	{
	    a[i].x = read() , a[i].y = read() , a[i].z = read() ;
	}
	sort(a + 1 , a + 1 + m , cmp) ;
	for(int i = 1 ; i <= m ; i ++)
	{
	    int fx = Find(a[i].x) , fy = Find(a[i].y) , ffx = Find(a[i].x + n) , ffy = Find(a[i].y + n) ;
	    if(fx == fy) 
	    {
	    	cout << a[i].z << endl ; 
	        return 0 ;
		}
		fa[fx] = ffy , fa[fy] = ffx ;
	}
	cout << 0 << endl ;
	return 20210809 ;
}

int true_answer = StarMaster() ;

int main() {;} 

可持久化并查集 :

内容:

其实已经基本上脱离了并查集 ,查询新 \(or\) 旧版本的关系, 可持久化并查集 = 可持久化 + 并查集 = 可持久化数组 + 并查集 = 主席树 + 并查集

操作:

主席树操作 + 并查集操作

例题:

P3402可持久化并查集

题目描述

给定 \(n\) 个集合,第 \(i\) 个集合内初始状态下只有一个数,为 \(i\)

\(m\) 次操作。操作分为 \(3\) 种:

  • 1 a b 合并 \(a~~,~~b\) 所在集合;
  • 2 k 回到第 \(k\) 次操作(执行三种操作中的任意一种都记为一次操作)之后的状态;
  • 3 a b 询问 \(a~,~b\) 是否属于同一集合,如果是则输出 \(1\) ,否则输出 \(0\)

输入格式

第一行两个整数 : \(n~~,~~m\)

接下来 \(m\) 行,每行先输入一个数 \(opt\)。若 \(opt=2\) 则再输入一个整数 \(k\),否则再输入两个整数 \(a~,~b\),描述一次操作。

输出格式

对每个操作 \(3\),输出一行一个整数表示答案。

分析:

\(fa[x]\) 数组建主席树来查询各个版本的情况 ,

其实真没啥好分析的

代码:

#include<bits/stdc++.h>

using namespace std ;
const int MM = 1e7 + 5000000 ;

int n , m , fa[MM] , a[MM] , siz , rt[MM] , num[MM] , tot ;
struct Tree
{
    int val , ls , rs , d ;
}tr[MM] ;
inline int read()
{
	int x = 0 , f = 1 ;
	char ch = getchar();
	while(ch > '9' or ch < '0')
	{
		if(ch == '-') f = -1 ;
		ch = getchar() ;
	}
	while(ch >= '0' and ch <= '9')
	{
		x = (x << 1) + (x << 3) + (ch ^ 48) ;
		ch = getchar() ;
	}
	return x * f ;
}
inline void build(int &p, int l , int r)
{
    p = ++siz ;
    if(l == r)
    {
        fa[p] = l ;
        return ;
    }
    int mid = (l + r) >> 1 ;
    build(tr[p].ls , l , mid) ;
    build(tr[p].rs , mid + 1 , r) ;
}
inline void update(int &p , int pre , int l , int r , int x , int ffa)
{
    p = ++siz ;
    if(l == r)
    {
        fa[p] = ffa ;
        tr[p].d = tr[pre].d ;
        return ;
    }
    tr[p].ls = tr[pre].ls , tr[p].rs = tr[pre].rs ;
    int mid = (l + r) >> 1 ;
    if(x <= mid) update(tr[p].ls , tr[pre].ls , l ,  mid , x , ffa);
    else update(tr[p].rs , tr[pre].rs , mid + 1 , r , x , ffa) ; 
}

inline int query(int p , int l , int r , int x)
{
    if(l == r) return p ;
    int mid = (l + r) >> 1 ;
    if(x <= mid) return query(tr[p].ls , l , mid , x) ;
    else return query(tr[p].rs , mid + 1 , r , x) ;
}
inline int Find(int num,int x)
{
    int f = query(num , 1 , n , x);
    if(x == fa[f]) return f;
    return Find(num , fa[f]);
}
inline void add(int p , int l , int r , int x)
{
    if(l == r) 
    { 
        tr[p].d ++;     
        return ;
    }
    int mid = (l + r) >> 1;
    if(x <= mid) add(tr[p].ls , l , mid , x) ;
    else add(tr[p].rs , mid + 1 , r , x);
}
inline void merge(int fx , int fy , int k)
{
	if(tr[fx].d > tr[fy].d) swap(fx , fy) ;
	update(num[k] , num[k - 1] , 1 , n , fa[fx] , fa[fy]) ;
	add(num[k] , 1 , n , fa[fy]) ;
	return ;
} 

inline void ioi()
{
	n = read() , m = read() ;
	build(num[0] , 1 , n) ;
	for(int i = 1 ; i <= m ; i ++)
	{
		int opt = read() ;
		if(opt == 1)
		{
			num[i] = num[i - 1] ;
			int x = read() , y = read() ;
			int fx = Find(num[i] , x) ;
			int fy = Find(num[i] , y) ;
			if(fx == fy) continue ;
			merge(fx , fy , i) ;
		}
		else if(opt == 2)
		{
			int k = read() ;
			num[i] = num[k] ;
		}
		else
		{
			num[i] = num[i - 1] ;
			int x = read() , y = read() ;
			int fx = Find(num[i - 1] , x) , fy = Find(num[i - 1] , y) ;
			if(fx == fy) cout << 1 << '\n' ;
			else cout << 0 << '\n' ;
		}
	}
	return ;
}

int main()
{
    ioi() ;
    return 0 ;
}

O完结V撒花O

感谢观赏

蒟蒻求赞 qwq

posted @ 2021-07-09 16:24  theStarMaster  阅读(1072)  评论(7)    收藏  举报