今天初学并查集,用最基本的原理题来练习,欢迎交流指正!
## 题目描述
如题,现在有一个并查集,你需要完成合并和查询操作。
## 输入格式
第一行包含两个整数 $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` 。
## 样例 #1
### 样例输入 #1
```
4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4
```
### 样例输出 #1
```
N
Y
N
Y
```
我们知道,并查集就是合并与查询两个操作,虽然看起来很容易,但是如果没有巧妙的算法数据结构,就很难用代码实现这个问题。
提示一下,可以考虑用 树 这一数据结结构,用树就需要有个一数组pre[ ](用于记录每个节点的父节点,才能对树进行遍历)。你或许有些思路了。下面我们详细解释他的实现。
1:
我们可以先把这n个元素看成单一的集合,也就是单一的树。所以我们对其进行初始化,将这n个元素的前驱节点,也就是父节点pre[]设置成他本身,就代表一颗独树。然后我们对这些元素进行合并查询就容易的多了。
2:
接下来需要干什么?没错,因为我们要执行多次查找,合并,所有你应该定义查找,合并这两个函数,我用add(),seek()代表。
1): add()函数,把y元素的根节点的父节点设置为 x元素的根节点即可,这样就把两个在不同树下的元素连接成了同一颗树。不明白可以看图
2): seek()函数,递归的查询x节点父节点,知道当前节点的父节等于他本身,则说明查找到了根节点。返回根节点即可。
这里需要注意,seek函数,查询,我们对于树的查询,是从当前节点往根节点进行遍历,所以时间复杂度就是当前节点的深度。如果每次查询时间复杂度都是2^根深,那样时间复杂度就太大了。那我们怎么解决呢? 可以在一边查询,一边路径压缩。为什么?
因为我们这道题查询的是根节点,所以各个子节点之间的关系并不重要(也就是只要保证子节点都在根下面即可),!!!敲黑板!!!我们只需要在遍历查找的时候把遍历过的元素节点的父节点设置为该树的根节点即可,最后我们的树就会从图一变为图二,这样后面查找根节点的时候,时间复杂度就大大降低了
关键代码:pre[x] = seek(pre[x]);//递归查找根节点,同时设置路过节点的父节点为根节点
这就是路径压缩,前提是各个子节点的顺序可以换
代码来咯!!!
#include<bits/stdc++.h>
using namespace std;
int pre[10005];//pre数组记录每个元素的父节点元素
int seek(int x)
{
if(pre[x] != x)//当前节点不是根节点
{
pre[x] = seek(pre[x]);//递归查找根节点,同时设置路过节点的父节点为根节点
}
return pre[x];//如果查找到根节点,就返回pre[x],即根节点。不能返回x,因为x作为带查询的节点一直没变,变的是pre[x],一直更新为父节点,直到更新为根节点
}
void add(int x, int y)
{
int a = seek(x);
int b = seek(y);
pre[b] = a;//让y的根节点的父节点变为x的根节点,相当于合并了两颗子树
}
int main()
{
int n,m;
cin >> n >> m ;
for(int i = 1; i < n+1; i++)
{
pre[i] = i;//初始化所有元素的父节点为他本身
}
for(int i = 0; i < m; i++)
{
int k,x,y;
cin >> k >> x >> y;
if(k == 1)//合并
{
add(x,y);
}
else//查询
{
if(seek(x) == seek(y))//如果两个元素的根节点相同,说明是同一个集合
{
cout << 'Y' << endl;
}
else cout << 'N' << endl;
}
}
}