并查集
板子
int find(int x);
void uniona(int x, int y);
------------------------------------------------------------
int p[N]; //p是父亲数组
for(int i = 1; i <= n; i++) p[i] = i; //初始化
int p1 = find(a), p2 = find(b); //单独写出来,避免多次调用函数;p1是a的根节点,p2是b的根节点
if(p1 == p2); //在一个集合中
else uniona(p1, p2); //不在一个集合中
int find(int x) //寻找根节点 + 路径压缩
{
int a = x; //保存x的值,为下面路径压缩做准备
while(a != p[a]) a = p[a]; //递推寻找根节点
int b = x;
while(b != a) //写p[b] != a也可以,因为是根节点的时候,p[b] = b
{
int c = p[b];
p[b] = a;
b = c; //为下一次循环将b原来的父亲的父亲设为根节点做准备
}
return a;
}
void uniona(int x, int y)
{
p[find(x)] = find(y);
}
p[x] = y ; //x的父亲是y
路径压缩
将每个点的父亲都设为根节点,这样会更加方便
用递推 ——> 递归可能会导致溢出栈
>1【模板】并查集
题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
输入格式
第一行包含两个整数 \(N,M\) ,表示共有 \(N\) 个元素和 \(M\) 个操作。
接下来 \(M\) 行,每行包含三个整数 \(Z_i,X_i,Y_i\) 。
当 \(Z_i=1\) 时,将 \(X_i\) 与 \(Y_i\) 所在的集合合并。
当 \(Z_i=2\) 时,输出 \(X_i\) 与 \(Y_i\) 是否在同一集合内,是的输出
Y
;否则输出 N
。
输出格式
对于每一个 \(Z_i=2\) 的操作,都有一行输出,每行包含一个大写字母,为 Y
或者 N
。
样例
4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4
N
Y
N
Y
提示
对于 \(100\%\) 的数据,\(1\le N \le 10^4\),\(1\le M \le 2\times 10^5\),\(1 \le X_i, Y_i \le N\),\(Z_i \in \{ 1, 2 \}\)。
代码
#include <iostream>
using namespace std;
const int N = 1e4 + 10;
int p[N];
int find(int x);
void uniona(int x, int y);
int main()
{
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++) p[i] = i;
while(m--)
{
int z, x, y; cin >> z >> x >> y;
int p1 = find(x), p2 = find(y);
if(z == 1) uniona(p1, p2);
else
{
if(p1 == p2) cout << "Y" << endl;
else cout << "N" << endl;
}
}
}
int find(int x)
{
int a = x;
while(a != p[a]) a = p[a];
int b = x;
while(b != a)
{
int c = p[b];
p[b] = a;
b = c;
}
return a;
}
void uniona(int x, int y)
{
p[x] = y;
}
>2 亲戚
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:\(x\) 和 \(y\) 是亲戚,\(y\) 和 \(z\) 是亲戚,那么 \(x\) 和 \(z\) 也是亲戚。如果 \(x\),\(y\) 是亲戚,那么 \(x\) 的亲戚都是 \(y\) 的亲戚,\(y\) 的亲戚也都是 \(x\) 的亲戚。
输入格式
第一行:三个整数 \(n,m,p\),(\(n,m,p \le 5000\)),分别表示有 \(n\) 个人,\(m\) 个亲戚关系,询问 \(p\) 对亲戚关系。
以下 \(m\) 行:每行两个数 \(M_i\),\(M_j\),\(1 \le M_i,~M_j\le n\),表示 \(M_i\) 和 \(M_j\) 具有亲戚关系。
接下来 \(p\) 行:每行两个数 \(P_i,P_j\),询问 \(P_i\) 和 \(P_j\) 是否具有亲戚关系。
输出格式
\(p\) 行,每行一个 Yes
或 No
。表示第 \(i\) 个询问的答案为“具有”或“不具有”亲戚关系。
样例
6 5 3
1 2
1 5
3 4
5 2
1 3
1 4
2 3
5 6
Yes
Yes
No
代码
#include <iostream>
using namespace std;
const int N = 5010;
int p[N];
int find(int x);
void uniona(int x, int y);
int main()
{
int n, m, q; cin >> n >> m >> q;
for(int i = 1; i <= n; i++) p[i] = i;
while(m--)
{
int x, y; cin >> x >> y;
uniona(find(x), find(y));
}
while(q--)
{
int x, y; cin >> x >> y;
if(find(x) == find(y)) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
int find(int x)
{
int a = x;
while(a != p[a]) a = p[a];
int b = x;
while(b != a)
{
int c = p[b];
p[b] = a;
b = c;
}
return a;
}
void uniona(int x, int y)
{
p[x] = y;
}
>3 朋友
题目背景
小明在 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\)。
输出格式
输出一行一个正整数,表示通过小明和小红认识的人最多一共能配成多少对情侣(包括他们自己)。
样例
4 3 4 2
1 1
1 2
2 3
1 3
-1 -2
-3 -3
2
提示
对于 \(100 \%\) 的数据,\(N,M \le 10^4\),\(P,Q \le 2 \times 10^4\)。
代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e4 + 10;
int pa[N], pb[N], cnta[N], cntb[N]; //分开统计男女朋友数,最小值就是所求
int finda(int x);
int findb(int x);
int main()
{
int n, m, k, h; cin >> n >> m >> k >> h;
for(int i = 1; i <= n; i++) pa[i] = i, cnta[i] = 1;
for(int i = 1; i <= m; i++) pb[i] = i, cntb[i] = 1; //将负值变为正值更方便
while(k--)
{
int x, y; cin >> x >> y;
int p1 = finda(x), p2 = finda(y);
if(p1 != p2) //将1作为男生总的根节点
{
if(p1 == 1) pa[p2] = p1, cnta[p1] += cnta[p2];
else if(p2 == 1) pa[p1] = p2, cnta[p2] += cnta[p1];
else pa[p1] = p2, cnta[p2] += cnta[p1];
}
}
while(h--)
{
int x, y; cin >> x >> y;
x = -x, y = -y;
int p1 = findb(x), p2 = findb(y);
if(p1 != p2) //将-1作为女生总的根节点,负变正之后就是将1作为总的根节点
{
if(p1 == 1) pb[p2] = p1, cntb[p1] += cntb[p2];
else if(p2 == 1) pb[p1] = p2, cntb[p2] += cntb[p1];
else pb[p1] = p2, cntb[p2] += cntb[p1];
}
}
int num = min(cnta[1], cntb[1]);
cout << num;
}
int finda(int x)
{
int a = x;
while(a != pa[a]) a = pa[a];
int b = x;
while(b != a)
{
int c = pa[b];
pa[b] = a;
b = c;
}
return a;
}
int findb(int x)
{
int a = x;
while(a != pb[a]) a = pb[a];
int b = x;
while(b != a)
{
int c = pb[b];
pb[b] = a;
b = c;
}
return a;
}
>4 银河英雄传说
简化版题目
A将战舰划分成\(30000\)列,每列依次编号为\(1, 2,...,30000\)。战舰也依次编号为 \(1, 2, ..., 30000\),让第\(i\) 号战舰处于第$ i$ 列。
合并指令为\(M\) \(i\) \(j\),第\(i\)号战舰整体(头在前尾在后)接至第 \(j\)号战舰所在队列的尾部。战舰队列是由处于同一列的战舰组成的。
\(B\)在\(A\)发布指令调动舰队的同时,也会发出一些询问指令:\(C\) \(i\) \(j\)。\(A\)的第\(i\)号战舰与第\(j\)号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
输入格式
第一行有一个整数 \(T\)(\(1 \le T \le 5 \times 10^5\)),表示总共有 \(T\) 条指令。
以下有 \(T\) 行,每行有一条指令。指令有两种格式:
-
M i j
:\(i\) 和 \(j\) 是两个整数(\(1 \le i,j \le 30000\)),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第 \(i\) 号战舰与第 \(j\) 号战舰不在同一列。 -
C i j
:\(i\) 和 \(j\) 是两个整数(\(1 \le i,j \le 30000\)),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。
输出格式
- 调动指令不要输出任何信息。
- 询问指令:在同一列上,输出第 \(i\) 号战舰与第 \(j\) 号战舰之间布置的战舰数目。不在同一列上,则输出 \(-1\)。
样例
4
M 2 3
C 1 2
M 2 4
C 4 2
-1
1
提示
样例解释
战舰位置图:表格中阿拉伯数字表示战舰编号。
题目中没有强制 \(i \neq j\),但是实测数据中不存在 \(i = j\) 的情况。
代码
#include <iostream>
using namespace std;
const int N = 3e4 + 10;
int p[N], cnt[N], d[N]; //cnt记录每一列的数量, d记录该点到根节点的距离
int find(int x);
void uniona(int p1, int p2);
int main()
{
int t; cin >> t;
for(int i = 1; i <= 30000; i++) p[i] = i, cnt[i] = 1;
while(t--)
{
char c;
int a, b; cin >> c >> a >> b;
int p1 = find(a), p2 = find(b);
if(c == 'M') uniona(p1, p2);
else
{
if(p1 != p2) cout << -1 << endl;
else cout << abs(d[a] - d[b]) - 1 << endl; //初始d[i]为0,所以计算中间个数的时候要减1
}
}
}
int find(int x)
{
if(p[x] == x) return x;
int p1 = p[x], p2 = find(p[x]);
p[x] = p2;
d[x] += d[p1];
return p2;
}
void uniona(int p1, int p2)
{
p[p1] = p2;
d[p1] = cnt[p2];
cnt[p2] += cnt[p1];
cnt[p1] = 0;
}