并查集
对于某些集合问题,涉及到大量查询操作(查询两个元素是否属于同一集合或是查询某一个元素的祖宗节点),如果使用一般的数据结构,时间和空间开销都比较大,而并查集则可以高效地解决这一类问题。
并查集是一种树状数据结构,用来解决集合间的合并以及元素的查询问题,每个集合可以理解为一棵树,不同的集合构成一个森林。
我们来看并查集的一个简单的应用:
1.Acwing836.合并集合
一共有n个数,编号是1~n,最开始每个数各自在一个集合中。
现在要进行m个操作,操作共有两种:
1.“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
2.“Q a b”,询问编号为a和b的两个数是否在同一个集合中;
输入格式
第一行输入整数n和m。
接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种
输出格式
对于每个询问指令”Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。
每个结果占一行。
数据范围:
这就是并查集的一个最简单的应用,一开始,每个集合中都只有一个点,它们没有任何联系,可以理解为由n个孤立的根节点组成的森林,之后每次进行合并操作,我们先查询这两个点是否再一个集合中,由于一开始所有点都是孤立的,所以查询失败(即两点不在一个集合中),我们让其中一点的祖宗节点的祖宗等于另一个节点,即"p[ find(x) ] = y;"(find函数是实现对某个点查找其祖宗节点并进行路径压缩)这样,相当于x有一个父节点y,实现了元素间的合并操作,对于查询操作,只需要对比两个元素的祖宗节点是否是一个节点即可。
#include <iostream>
using namespace std;
const int N = 1e5+100;
int p[N];
int n,m,a,b;
//并查集的核心操作
int find(int x)
{
if(p[x] !=x) p[x] = find(p[x]); //路径压缩
return p[x];
}
int main()
{
cin>>n>>m;
for(int i = 1; i<=n ;i++) p[i] = i; //初始化
char s;
while(m--)
{
cin>>s>>a>>b;
if(s == 'M') p[find(a)] = find(b);
else
{
if(find(a) != find(b))
cout<<"No"<<endl;
else
cout<<"Yes"<<endl;
}
}
return 0;
}
2.Acwing1250.格子游戏
Alice和Bob玩了一个古老的游戏:首先画一个 的点阵
接着,他们两个轮流在相邻的点之间画上红边和蓝边:
![]()
直到围成一个封闭的圈为止,“封圈”的那个人就是赢家。因为棋盘实在是太大了,他们的游戏实在是太长了!
他们甚至在游戏中都不知道谁赢得了游戏。
于是请你写一个程序,帮助他们计算他们是否结束了游戏?
输入格式
输入数据第一行为两个整数 n 和 m。n表示点阵的大小,m 表示一共画了 m 条线。
以后 m 行,每行首先有两个数字 (x,y),代表了画线的起点坐标,接着用空格隔开一个字符,假如字符是 D,则是向下连一条边,如果是 R 就是向右连一条边。
输入数据不会有重复的边且保证正确。
输出格式
输出一行:在第几步的时候结束。
假如 m 步之后也没有结束,则输出一行“draw”。
数据范围
1≤n≤200,
1≤m≤24000
我们将连过线的点通过并查集的合并操作合并到一个集合中,之后每次划线前,我们只要判断即将要连线的两点是否已经属于同一集合,若属于,则游戏结束。否则合并这两点。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5;
int n,m,a,b;
int p[N];
char q;
int trans(int a,int b)
{
return a * n + b;
}
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin>>n>>m;
for(int i = 0; i<n*n; i++) p[i] = i;
int pa,pb,ans = 0;
for(int i = 1; i<=m; i++)
{
cin>>a>>b>>q;
int pa = trans(--a,--b);
if(q == 'D')
pb = trans(a+1,b);
else
pb = trans(a,b+1);
pa = find(pa), pb = find(pb);
if(pa == pb)
{
ans = i;
break;
}
p[pa] = pb;
}
if(ans) cout<<ans<<endl;
else cout<<"draw"<<endl;
return 0;
}
3.Acwing1252.搭配购买
Joe觉得云朵很美,决定去山上的商店买一些云朵。
商店里有 n 朵云,云朵被编号为 1,2,…,n,并且每朵云都有一个价值。
但是商店老板跟他说,一些云朵要搭配来买才好,所以买一朵云则与这朵云有搭配的云都要买。
但是Joe的钱有限,所以他希望买的价值越多越好。
输入格式
第 1 行包含三个整数 n,m,w,表示有 n 朵云,m 个搭配,Joe有 w 的钱。
第 2∼n+1行,每行两个整数 ci,di 表示 i 朵云的价钱和价值。
第 n+2∼n+1+m 行,每行两个整数 ui,vi,表示买 ui 就必须买 vi,同理,如果买 vi 就必须买 ui。
输出格式
一行,表示可以获得的最大价值。
数据范围
1≤n≤10000,
0≤m≤5000,
1≤w≤10000,
1≤ci≤5000,
1≤di≤100,
1≤ui,vi≤n
对于每个云朵的搭配,我们使用并查集的合并操作将必须一起购买的云朵合并到一个集合中并视作一个物品,它的价值就是集合中所包含的所有物品价值的和,价钱同理,在做完合并之后,每个集合等效地看待成一个物品,由于我们只有有限的钱,题目问获取的最大价值是多少,做一遍01背包即可。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
using namespace std;
const int N = 1e4+100;
int n,m,wb;
int p[N];
int f[N];
int v[N],w[N];
int find(int x)
{
if(p[x]!=x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin>>n>>m>>wb;
for(int i = 1; i<=n; i++) p[i] = i;
int a,b;
for(int i = 1; i<=n; i++) cin>>v[i]>>w[i];
int pa,pb;
for(int i = 1; i<=m; i++)
{
cin>>a>>b;
pa = find(a), pb = find(b);
if(pa != pb)
{
p[pa] = pb;
v[pb] += v[pa];
w[pb] += w[pa];
}
}
for(int i = 1; i<=n; i++)
if(p[i] == i)
for(int j = wb; j>=v[i]; j--)
f[j] = max(f[j],f[j-v[i]]+w[i]);
cout<<f[wb]<<endl;
return 0;
}
4.Acwing237.程序自动分析
在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。
考虑一个约束满足问题的简化版本:假设x1,x2,x3,…代表程序中出现的变量,给定n个形如xi=xj或xi≠xj的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。
例如,一个问题中的约束条件为:x1=x2,x2=x3,x3=x4,x1≠x4,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
现在给出一些约束满足问题,请分别对它们进行判定。
输入格式
输入文件的第1行包含1个正整数t,表示需要判定的问题个数,注意这些问题之间是相互独立的。对于每个问题,包含若干行:
第1行包含1个正整数n,表示该问题中需要被满足的约束条件个数。
接下来n行,每行包括3个整数i,j,e,描述1个相等/不等的约束条件,相邻整数之间用单个空格隔开。若e=1,则该约束条件为xi=xj;若e=0,则该约束条件为xi≠xj。
输出格式
输出文件包括t行。
输出文件的第k行输出一个字符串“YES”或者“NO”(不包含引号,字母全部大写),“YES”表示输入中的第k个问题判定为可以被满足,“NO”表示不可被满足。
数据范围
1≤n≤1000000
1≤i,j≤1000000000
首先,我们将所有满足' = '关系的元素利用并查集的合并操作合并到同一个集合中,然后,再依次遍历所有具有' ≠ '关系的两个元素进行查询操作,如果两个元素属于同一集合,则说明发生矛盾,即不能满足所有的约束条件。
此外,尽管下标i,j范围很大,但是n的范围小得多,所以我们对i与j做一个离散化处理,不要求保序,所以直接用哈希表或者map做。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unordered_map>
using namespace std;
const int N = 2e6+100;
int n,t,idx;
int p[N];
unordered_map<int,int> h;
struct Query{
int x,y,e;
}query[N];
int trans(int x)
{
if(h.count(x) == 0) h[x] = ++idx;
return h[x];
}
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d",&t);
while(t --)
{
idx = 0;
h.clear();
scanf("%d",&n);
int x,y,e;
for(int i = 0; i<n; i++)
{
scanf("%d%d%d",&x,&y,&e);
query[i] = {trans(x),trans(y),e};
}
for(int i = 1; i<=idx; i++) p[i] = i;
for(int i = 0; i<n ;i++)
if(query[i].e == 1)
{
int pa = find(query[i].x), pb = find(query[i].y);
p[pa] = pb;
}
bool ans = false;
for(int i = 0; i<n; i++)
if(query[i].e == 0)
{
int pa = find(query[i].x), pb = find(query[i].y);
if(pa == pb)
{
ans = true;
break;
}
}
if(ans) puts("NO");
else puts("YES");
}
return 0;
}
5.洛谷P1195口袋的天空
题目背景
小杉坐在教室里,透过口袋一样的窗户看口袋一样的天空。
有很多云飘在那里,看起来很漂亮,小杉想摘下那样美的几朵云,做成棉花糖。
题目描述
给你云朵的个数NN,再给你MM个关系,表示哪些云朵可以连在一起。
现在小杉要把所有云朵连成KK个棉花糖,一个棉花糖最少要用掉一朵云,小杉想知道他怎么连,花费的代价最小。
输入格式
每组测试数据的
第一行有三个数N,M,K
接下来MM个数每行三个数X,Y,LX,Y,L,表示XX云和YY云可以通过LL的代价连在一起。
输出格式
对每组数据输出一行,仅有一个整数,表示最小的代价。
如果怎么连都连不出KK个棉花糖,请输出'No Answer'。
开始我们可以把每一个点都当成一棵独立的生成树,因为要求满足条件的最小代价,因此我们可以利用Kruskal算法来求解,将所有的边按照权值从小到大进行排序,依次遍历所有边,利用并查集来维护所有的生成树,一旦该图中含有k个生成树,我们就可以得到答案。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n,m,k;
int p[N];
struct Edge{
int x,y,l;
bool operator<(const Edge& w)const{
return l<w.l;
}
}edge[N];
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edge,edge+m);
for(int i = 1; i<=n; i++) p[i] = i;
int res = n, v = 0;
for(int i = 0; i<m; i++)
{
if(res<=k) break;
int pa = find(edge[i].x), pb = find(edge[i].y);
if(pa != pb)
{
p[pa] = pb;
res --;
v += edge[i].l;
}
}
if(res == k) return v;
return -1;
}
int main()
{
cin>>n>>m>>k;
for(int i = 0; i<m; i++)
scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].l);
int ans = kruskal();
cout<<ans<<endl;
return 0;
}
网友评论