并查集定义
并:Union(合并)——合并两个集合。 查:Find(查找)——判断两个元素是否在一个集合。 集:Set(集合)
并查集使用father数组实现:
int father[N];
father[i] = x; //表示节点i的父亲节点为x
father[i] = i; //当父亲节点为本身时,那么该节点为根节点
并查集的基本操作
1.初始化
首先初始化,将父亲节点都设置为自己。
void init(int n){
for(int i = 1;i <= n;i++) {
father[i] = i;
}
}
2.查找
一个集合只存在一个根节点,可以使用递归或者递推实现根节点的查找。代码如下:
//递推实现
int findFather(int x) {
while(x != father[x]) {
x = father[x];
}
return x;
}
//递归实现
int findFather(int x) {
if(x == father[x]) return x;
else findFather(father[x]);
}
3.合并
简而言之,合并是指将两个集合合并成一个集合,也就是将两个集合中的其中一个集合的根节点的父亲节点设置成另外一个集合的根节点。
算法步骤:
1.首先判断给定的两个节点a,b是否属于同一个集合。(调用findFather函数,将返回值比较) 2.合并两个集合:将father[findFather(a)] = findFather(b);(同father[findFather(b)] = findFather(a);)
代码如下:
void Union(int a,int b){
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB)
father[faA] = faB;
}
路径压缩
路径压缩是一个优化方式,如果树高太高,那么每次查找根节点就需要花费很大的计算量查找。查找的目的就是找到根节点,我们可以将它等价变换,如下图:
这样我们就可以减少我们查找根节点时的计算量。代码如下:
int findFather(int x) {
int a = x;
while(x!=father[x]) {
x = father[x];
}
//路径压缩(将该路径上的所有的节点指向根节点)
while(a!=father[a]) {
int z =a ;
a = father[a];
father[z] = x;
}
return x;
}
Robocom 真题 7-4 疫情防控
题目描述
疫情尚未结束,严防疫情反复。为了做好疫情防控工作,国内设置了地区风险等级,对于中高风险地区的人员采取限制移动、居家隔离等手段。
为了研究疫情防控对于跨地区交通运输的影响,假设现在有 N 个机场,M 条航线,每天都会新增一个防控地区,一个防控地区会导致一个机场无法正常运作,航线也自然无法正常运行,每天会有 Qi 对旅客从 Xi 机场前往 Yi 机场,请计算有多少对旅客会受到影响无法完成行程。
旅客只要能直达或通过若干次中转,且乘坐的所有航线的出发和到达机场都正常运作,即视作可完成行程。
输入格式
输入第一行是三个整数 N,M,D (1≤N≤5×10^4, 1≤M≤2×10^5, 1≤D≤10^3), 表示机场数、航线数以及新增防控地区的天数。
接下来首先有 M 行,每行给出空格分隔的两个数字 A 和 B,表示编号为 A 和 B 的机场之间有一条航线。航线是双向的,机场编号从 1 到 N。
然后是 D 块输入,每块输入内第一行为空格分隔的两个整数 C 和 Q (1≤Q≤103),表示新增机场编号为 C 所在的城市为防控地区,今天有 Q 段行程。数据保证新增的城市之前一定不是防控地区。
接下来的 Q 行,每行是空格分隔的两个数字 X 和 Y,表示编号为 X 和 Y 的机场的一段行程。行程有可能包括之前就已经成为防控地区的城市。
输出格式
对于每天的询问,请在一行中输出在新增了一个防控地区后当天的行程有多少不能成行。
输入样例
5 5 3
1 2
1 3
1 5
2 5
3 4
4 3
1 3
1 4
2 3
5 3
3 4
2 3
3 5
1 3
2 3
2 5
3 4
输出样例
1
2
3
题解
本题题意:给出n个点,m条边的无向图,然后在接下来的d天每天删除一个c点,然后询问q次是否能够连通。
本题的主要知识点为并查集及其路径压缩。
解题思路:拿到本题知道题意后,首先会想删除无向图中的点,每删除一个点就要重建一次图(本人才疏学浅,关于删除点只想到了重建图),因此,这样做的时间复杂度是非常高的,因此我们可以反过来想想,是否可以从最后一天依次加点呢?本人抱着试一试的想法,没想到真滴过了。下面我来讲讲这道题的解决过程:
1.首先把需要的数组、头文件准备好
#include <iostream>
#include <vector>
#include <map>
using namespace std;
const int N = 50005; //数组一定要开到足够大,不然过题会出现“段错误”
int father[N+10]; //存放父亲节点
int a[200005],b[200005]; //存放输入的边
int c[1005],q[1005]; //存放删除的点、询问次数
struct nn{
int a;
int b;
}; //该结构体用于存放询问时输入的边
注意:数组的范围需要开到足够大!!!
2.写出并查集的模板函数
//初始化数组
void init(int n) {
for(int i = 1;i <= n;i++) {
father[i] = i;
}
}
//找到父亲节点
int findFather(int x) {
int a = x;
while(x != father[x]) {
x = father[x];
}
//优化——路径压缩
while(a != father[a]) {
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
//加点,集合合并
void Union(int a,int b) {
int faA = findFather(a);
int faB = findFather(b);
if(faA != faB)
father[faA] = faB;
}
具体的函数过程在上文已经有所说明。(若有不懂可以划上去再悟悟,如果仍然不懂可以去找找并查集的其他博客学习)
注意:这里必须用到路径压缩,最后一个点的范围非常大,如果不进行路径压缩优化会超时。
3.输入信息
int n,m,d;
cin>>n>>m>>d;
init(n+1);//初始化
for(int i = 0;i < m;i++) {
cin >> a[i] >> b[i]; //输入边
}
map<int,vector<nn> > rec2; //用于存放询问的内容
map<int,bool> juge; //判断该点是否删除
for(int i = 0;i < d;i++) {
cin >> c[i] >> q[i];
for(int j = 0;j < q[i];j++){
nn x;
cin >> x.a >> x.b;
rec2[i].push_back(x);
}
juge[c[i]] = 1; //由于是倒着来模拟的,因此这里需要将所有删除的点都标记
}
输入信息时不要着急运算或者模拟,本题采用的是逆向思维,因此需要先将输入的信息储存起来,在后面的解题中再去调用。(容器需要慎重选择,容易出错)
4.构建初始图,也就是最后一天的图
for(int i =0 ;i < m;i++){
if(juge[a[i]] != 1&& juge[b[i]]!=1) {
Union(a[i],b[i]);
}
}
将没有删除的点连起来。
5.从最后一天开始向前模拟
vector<int> ans;//存放答案,倒着输出
for(int i = d-1;i>=0;i--) { //记录天数 i
int ansss = 0; //初始化ansss
juge[c[i]] = 0; //将这天删除的点恢复
//判断两点是否连通,也就是判断两点的根节点是否相同
for(int j = 0;j<q[i];j++) { //记录询问次数j
if(findFather(rec2[i][j].a) != findFather(rec2[i][j].b))
ansss++;
}
//加点,需要注意:要保证加入点要连通的另一点是未删除状态
for(int j = 0;j < m;j++) {
if((a[j] == c[i] || b[j] == c[i])&&(juge[a[j]]!=1 && juge[b[j]]!=1))
Union(a[j],b[j]);
}
ans.push_back(ansss);
}
本题到这就结束了,有什么不懂的、代码思路有误或者有其他更好的解法可以共同讨论。