算法自学__并查集

参考资料:

简单并查集

代码

int f[10001];
int Rank[10001];		//按秩合并
void init(int n) {		//初始化
	for (int i = 0; i < n; i++) {
		f[i] = i;
		Rank[i] = 1;
	}
}
int find(int x) {		//路径压缩
	return f[x] == x ? x : (f[x] = find(f[x]));
}
void merge(int i, int j) {		//合并
	int I = find(i), J = find(j);
	if (I == J) return;
	if (Rank[I] > Rank[J]) f[J] = I;
	else if (Rank[J] > Rank[I]) f[I] = J;
	else {
		f[I] = J;
		Rank[J]++;
	}
}

例1 P2661 [NOIP2015 提高组] 信息传递

题目描述

n n n 个同学(编号为 1 1 1 n n n )正在玩一个信息传递的游戏。在游戏里每人都有一个固定的信息传递对象,其中,编号为 i i i 的同学的信息传递对象是编号为 T i T_i Ti 的同学。

游戏开始时,每人都只知道自己的生日。之后每一轮中,所有人会同时将自己当前所知的生日信息告诉各自的信息传递对象(注意:可能有人可以从若干人那里获取信息, 但是每人只会把信息告诉一个人,即自己的信息传递对象)。当有人从别人口中得知自 己的生日时,游戏结束。请问该游戏一共可以进行几轮?

输入格式

2 2 2行。

1 1 1行包含1个正整数 n n n ,表示 n n n 个人。

2 2 2行包含 n n n 个用空格隔开的正整数 T 1 , T 2 , ⋯ ⋯   , T n T_1,T_2,\cdots\cdots,T_n T1,T2,⋯⋯,Tn ,其中第 i i i 个整数 T i T_i Ti 表示编号为 i i i 的同学的信息传递对象是编号为 T i T_i Ti 的同学, T i ≤ n T_i \leq n Tin T i ≠ i T_i \neq i Ti=i

输出格式

1 1 1个整数,表示游戏一共可以进行多少轮。

样例 #1

样例输入 #1
5
2 4 2 3 1
样例输出 #1
3

提示

对于 30 % 30\% 30%的数据, n ≤ 200 n ≤ 200 n200

对于 60 % 60\% 60%的数据, n ≤ 2500 n ≤ 2500 n2500

对于$ 100%$的数据, n ≤ 200000 n ≤ 200000 n200000

思路

根据信息的传递关系创建有向图,利用并查集在建图的过程中找到最小环的长度。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 200005;

int f[maxn];
int dis[maxn];
int n;
int ans = 0x7fffffff;
int a;

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

int find(int x){
	if(f[x]==x){
		return x;
	}
	else{
		int old = f[x];		
		f[x] = find(f[x]);
		//dis[old]表示原父结点到新父结点的距离;
		//当前结点到新父结点的距离为:到原父结点的距离(即dis[x]的旧值)
		//加上原父结点到新父结点的距离(即dis[old])。
		dis[x] += dis[old];
		return f[x];
	}
}

void merge(int x, int y){
	f[x] = y;		//不能颠倒!
	dis[x] = 1;
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin>>n;
	init();
	for(int i=1;i<=n;i++){
		cin>>a;
		if(find(i)==find(a)){
			// 实质上,dis[i]是恒为0的
			ans = min(dis[i]+dis[a]+1, ans);
		}
		else{
			merge(i, a);
		}
	}
	cout<<ans;
	return 0;
}

例2 P6121 [USACO16OPEN]Closing the Farm G

题目描述

FJ 和他的奶牛们正在计划离开小镇做一次长的旅行,同时 FJ 想临时地关掉他的农场以节省一些金钱。

这个农场一共有被用 M M M 条双向道路连接的 N N N 个谷仓( 1 ≤ N , M ≤ 2 × 1 0 5 1 \leq N,M \leq 2 \times 10^5 1N,M2×105)。为了关闭整个农场,FJ 计划每一次关闭掉一个谷仓。当一个谷仓被关闭了,所有的连接到这个谷仓的道路都会被关闭,而且再也不能够被使用。

FJ 现在正感兴趣于知道在每一个时间(这里的“时间”指在每一次关闭谷仓之前的时间)时他的农场是否是“全连通的”——也就是说从任意的一个开着的谷仓开始,能够到达另外的一个谷仓。注意自从某一个时间之后,可能整个农场都开始不会是“全连通的”。

输入格式

输入第一行两个整数 N , M N,M N,M

接下来 M M M 行,每行两个整数 u , v u,v u,v 1 ≤ u , v ≤ N 1 \leq u,v \leq N 1u,vN),描述一条连接 u , v u,v u,v 两个农场的路。

最后 N N N 行每行一个整数,表示第 i i i 个被关闭的农场编号。

输出格式

输出 N N N 行,每行包含 YESNO,表示某个时刻农场是否是全连通的。

第一行输出最初的状态,第 i i i 行( 2 ≤ i ≤ N 2 \leq i \leq N 2iN)输出第 i − 1 i-1 i1 个农场被关闭后的状态。

样例 #1

样例输入 #1
4 3
1 2
2 3
3 4
3
4
1
2
样例输出 #1
YES
NO
YES
YES

思路

将关闭农场倒推为开放农场,每开放一个农场,就将该农场与其连接的已开放的农场进行并查集merge()操作,并维护每个集合的元素个数,若某一时刻一个集合中的元素个数恰好为当前已开放的农场数,则此时农场为“全连通”的。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 2e5+5;

struct EDGE{
	int to;
	int next;
};

EDGE edge[maxn<<1];
int head[maxn];
int cnt = 0;
int f[maxn];
int siz[maxn];
int N, M, u, v;
int a[maxn];
bool open[maxn];
bool ans[maxn];

void addEdge(int x, int y){
	cnt++;
	edge[cnt].to = y;
	edge[cnt].next = head[x];
	head[x] = cnt;
}

void init(){
	for(int i=1;i<=N;i++){
		f[i] = i;
		siz[i] = 1;
	}
}

int find(int x){
	return x==f[x] ? x : (f[x] = find(f[x]));
}

void merge(int x, int y){
	int X = find(x), Y = find(y);
	if(X==Y) return;
	f[X] = Y;
	siz[Y] += siz[X];		//维护元素个数
}

int main(){
	cin>>N>>M;
	init();
	for(int i=1;i<=M;i++){
		cin>>u>>v;
		addEdge(u, v);
		addEdge(v, u); 
	}
	for(int i=0;i<N;i++){
		cin>>a[N-i];
	}
	open[a[1]] = true;
	ans[N] = true;
	for(int i=2;i<=N;i++){
		int cur = a[i];
		open[cur] = true;
		for(int j=head[cur];j!=0;j=edge[j].next){
			int to = edge[j].to;
			if(!open[to]) continue;
			merge(to, cur);
		}
		//只有集合中的代表元素的siz才有意义
		if(siz[find(cur)]==i) ans[N-i+1] = true;
	}
	for(int i=1;i<=N;i++){
		if(ans[i]) cout<<"YES\n";
		else cout<<"NO\n";
	}
	return 0;
}

种类并查集

例1 P1525 [NOIP2010 提高组] 关押罪犯

题目描述

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

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

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

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

输入格式

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

输出格式

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

样例 #1

样例输入 #1
4 6
1 4 2534
2 3 3512
1 2 28351
1 3 6618
2 4 1805
3 4 12884
样例输出 #1
3512

提示

【数据范围】

对于 30 % 30\% 30%的数据有 N ≤ 15 N\leq 15 N15

对于 70 % 70\% 70% 的数据有 N ≤ 2000 , M ≤ 50000 N\leq 2000,M\leq 50000 N2000,M50000

对于 100 % 100\% 100% 的数据有 N ≤ 20000 , M ≤ 100000 N\leq 20000,M\leq 100000 N20000,M100000

思路

本题主要利用“敌人的敌人是朋友”这一性质。在本题目中,“朋友”即居住在同一个监狱的人,“敌人”即居住在不同监狱的人。显然,如果AB住在不同的监狱,BC住在不同的监狱,则AC一定住在同一个监狱。

将罪犯对按照仇恨值从大到小排序,一次将每个罪犯对中的两个罪犯设为敌对关系,直到不能满足为止。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 2e4+5;
const int maxm = 1e5+5;

struct NODE{
	int a, b;
	int c;
	bool operator<(const NODE x)const{
		return c>x.c;
	}
};

NODE node[maxm];
int N, M;
int f[maxn<<1], r[maxn<<1];
int ans = 0;

void init(){
	for(int i=1;i<=2*N;i++){
		f[i] = i;
		r[i] = 1;
	} 
}

int find(int x){
	return x==f[x] ? x : (f[x]=find(f[x]));
}

bool query(int a, int b){
	return find(a)==find(b);
}

void merge(int x, int y){
	int X = find(x), Y = find(y);
	if(X==Y) return;
	if(r[X]>r[Y]) f[Y] = X;
	if(r[Y]>r[X]) f[X] = Y;
	if(r[X]==r[Y]){
		f[Y] = X;
		r[X]++;
	} 
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin>>N>>M;
	init();
	for(int i=1;i<=M;i++){
		cin>>node[i].a>>node[i].b>>node[i].c;
	}
	sort(node+1, node+1+M);
	for(int i=1;i<=M;i++){
		if(!query(node[i].a, node[i].b)){
			merge(node[i].a, node[i].b+N);
			merge(node[i].b, node[i].a+N);
		}
		else{
			ans = node[i].c;
			break;
		}
	}
	cout<<ans;
	return 0;
}

带权并查集

例1 P1196 [NOI2002] 银河英雄传说

题目背景

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

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

题目描述

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

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

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

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

输入格式

第一行有一个整数 T T T 1 ≤ T ≤ 5 × 1 0 5 1 \le T \le 5 \times 10^5 1T5×105),表示总共有 T T T 条指令。

以下有 T T T 行,每行有一条指令。指令有两种格式:

  1. M i j i i i j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1i,j30000),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第 i i i 号战舰与第 j j j 号战舰不在同一列。

  2. C i j i i i j j j 是两个整数( 1 ≤ i , j ≤ 30000 1 \le i,j \le 30000 1i,j30000),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。

输出格式

依次对输入的每一条指令进行分析和处理:

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

样例 #1

样例输入 #1
4
M 2 3
C 1 2
M 2 4
C 4 2
样例输出 #1
-1
1

思路

在并查集中维护siz[]数组和dis[]数组。siz[i]表示已i代表元素的集合中的元素个数;dis[i]表示i到本集合代表元素的距离。两个数组的维护方式见代码。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 3e4+5;
const int maxt = 5e5+5;

int T;
int n = maxn;
int f[maxn], siz[maxn], dis[maxn];

void init(){
	for(int i=1;i<=n;i++){
		f[i] = i;
		siz[i] = 1;
		dis[i] = 0;
	}
}

int find(int x){
	if(x==f[x]){
		return x;
	}
	else{
		int last = f[x];
		f[x] = find(f[x]);
		dis[x] += dis[last];
		return f[x];
	}
}

void merge(int a, int b){
	int x=find(a), y=find(b);
	if(x==y) return;
	else{
		f[x] = y;
		dis[x] = siz[y];	//插到队尾
		siz[y] += siz[x];
	}
}

int main(){
	cin>>T;
	init();
	for(int i=1;i<=T;i++){
		char c;
		int a, b;
		cin>>c>>a>>b;
		if(c=='M'){
			merge(find(a), find(b));
		}
		else{
			if(find(a)!=find(b)){
				cout<<-1<<'\n';
			}
			else{
				cout<<abs(dis[a]-dis[b])-1<<'\n';
			}
		}
	}
	return 0;
}

例2 P8779 [蓝桥杯 2022 省 A] 推导部分和

题目描述

对于一个长度为 N N N 的整数数列 A 1 , A 2 , ⋯ A N A_{1}, A_{2}, \cdots A_{N} A1,A2,AN,小蓝想知道下标 l l l r r r 的部分和 ∑ i = l r A i = A l + A l + 1 + ⋯ + A r \sum\limits_{i=l}^{r}A_i=A_{l}+A_{l+1}+\cdots+A_{r} i=lrAi=Al+Al+1++Ar 是多少?

然而,小蓝并不知道数列中每个数的值是多少,他只知道它的 M M M 个部分和的值。其中第 i i i 个部分和是下标 l i l_{i} li r i r_{i} ri 的部分和 ∑ j = l i r i = A l i + A l i + 1 + ⋯ + A r i \sum_{j=l_{i}}^{r_{i}}=A_{l_{i}}+A_{l_{i}+1}+\cdots+A_{r_{i}} j=liri=Ali+Ali+1++Ari, 值是 S i S_{i} Si

输入格式

第一行包含 3 个整数 N 、 M N 、 M NM Q Q Q。分别代表数组长度、已知的部分和数量 和询问的部分和数量。

接下来 M M M 行,每行包含 3 3 3 个整数 l i , r i , S i l_{i}, r_{i}, S_{i} li,ri,Si

接下来 Q Q Q 行,每行包含 2 2 2 个整数 l l l r r r,代表一个小蓝想知道的部分和。

输出格式

对于每个询问, 输出一行包含一个整数表示答案。如果答案无法确定, 输出 UNKNOWN

样例 #1

样例输入 #1
5 3 3
1 5 15
4 5 9
2 3 5
1 5
1 3
1 2
样例输出 #1
15
6
UNKNOWN

提示

对于所有评测用例, 1 ≤ N , M , Q ≤ 1 0 5 , − 1 0 12 ≤ S i ≤ 1 0 12 , 1 ≤ l i ≤ r i ≤ N 1 \leq N, M, Q \leq 10^{5},-10^{12} \leq S_{i} \leq 10^{12}, 1 \leq l_{i} \leq r_{i} \leq N 1N,M,Q105,1012Si1012,1liriN, 1 ≤ l ≤ r ≤ N 1 \leq l \leq r \leq N 1lrN 。数据保证没有矛盾。

蓝桥杯 2022 省赛 A 组 J 题。

思路

已知部分和 ( l , r , s ) (l, r, s) (l,r,s),我们可以利用差分的思想,将其转化为sum[r]-sum[l-1] = s,然后建立一条无向边 ( l − 1 , r ) (l-1, r) (l1,r)。对于每个询问 ( x , y ) (x, y) (x,y),我们只需判断 x − 1 x-1 x1 y y y 是否在同一个连通块内即可。
由于题目还要求我们给出区间和,所以我们可以使用带权并查集

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int maxn = 1e5+5;

int n, m, q;
int f[maxn], dis[maxn];

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

int find(int x){
	if(x!=f[x]){
		int old = f[x];
		f[x] = find(old);
		dis[x] += dis[old];
	}
	return f[x];
}

void merge(int a, int b, int c){
	int x=find(a), y=find(b);
	if(x==y) return;
	f[x] = y;
	dis[x] = dis[b]-(dis[a]-c);	//注意!必须更新父结点的值!
}

signed main(){
	cin>>n>>m>>q;
	init();
	for(int i=1;i<=m;i++){
		int a, b, c;
		cin>>a>>b>>c;
		a--;
		merge(a, b, c);
	}
	for(int i=1;i<=q;i++){
		int x, y;
		cin>>x>>y;
		x--;
		if(find(x)!=find(y)) cout<<"UNKNOWN\n";
		else{
			cout<<dis[x]-dis[y]<<'\n';
		}
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值