✅
Sets ☁️
上一题链接: C/C++百题打卡[6/100]——表格涂色「MCOI-06」Gerrymandering.
百题打卡总目录: 🚧 🚧 …
一、题目总述
● 输入样例:
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
二、思考空白区
● 题目难度:⭐️⭐️⭐️ 普及-
三、题目解析
● 题目的意思也比较明显,关键是我们要用什么样的 数据结构 来构建题目中所说的 “集合”。这里,我们要来学习一下闻名遐迩的 “并查集”。
● 并查集 [摘自百度百科]:并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。
● 一个涵盖了“并查集”的故事:【注意 公司名 的变化】
第一天,市场上有 5 个人(分别是 a、b、c、d、e),他们一开始都是自己的老板,即分别有 5 个单独的集合 “公司1_集合1{a}”、“公司2_集合{b}”、“公司3_集合{c}”、“公司4_集合{d}”、“公司5_集合{e}”,因为他们都在为自己打工赚钱。
第二天,小a 拉 小b 合伙一起干,但 “一山不容二虎”,不能有两个老板,小a 就说:“我做老板吧”。于是 小a 和 小b 组成了一个 “公司1_集合{a, b}”,这个集合中 小b 指向了 小a,因为 小a 做了 “最大的老板”。
第三天,小b 拉 小c 入伙,于是 小c 就不是自己的老板了,他的老板变成了 小b。 随后 “公司1_集合{a, b}” 更新为了 “公司1_集合{a, b, c}”
第四天,小d 拉 小e 入伙,小d 做了 “最大的老板”。故有了 “公司2_集合{d, e}”。
第五天,小c 对 小a 说:“大老板,我想喊 小e 入伙”。小c 就去拉 小e 了,小e 就说:“我去问问我的大老板”。结果他的大老板 小d 也同意了,于是 小d 的老板变成了 小c。故两家公司合并成了 “公司1_集合{a, b, c, d, e}”
● 这里提第一个问题,在一个 “集合” 中,怎么去找到 “自己最大的老板”?
这个可以通过 “指针+递归” 来处理。“指针” 体现在 “数组” 里。代码如下:
注: Boss[..] 是一个数组, x 的老板就是 Boss[x]
int Find_Big_Boss(int x)
{
if (Boss[x] == x) // 自己所属最大的老板是自己
return x;
else // 自己所属最大的老板不是自己
{
int Big_Boss = Find_Big_Boss(a[x]); // 通过递归去找到 自己的老板的 “所属的最大的老板”
return Big_Boss; // 找到 “最大的老板” 并返回
}
}
● 那么上面那个 故事 就可以演示为:
● 但如果按照上述 Find_Big_Boss()
函数来做的话,这这样可能会形成一条长长的链,随着链越来越长,我们想要从底部找到根节点会花越来越长的时间。怎么解决呢?
我们可以使用 路径压缩 的方法。既然我们只关心一个元素对应的根节点,那我们希望每个元素到根节点的路径尽可能短。这只要我们在查询的过程中,把 “沿途每个人(包括自己)的老板” 都设为 “最终找到的那个最大的老板” 即可。下一次再查询时,我们就可以省很多事。这用递归的写法很容易实现:
注: Boss[..] 是一个数组, x 的老板就是 Boss[x]
int Find_Big_Boss(int x)
{
if (Boss[x] == x) // 自己所属最大的老板是自己
return x;
else // 自己所属最大的老板不是自己
{
int Big_Boss = Find_Big_Boss( Boss[x] ); // 通过递归去找到 自己的老板的 “所属的最大的老板”
---------------->>> 只改了下面这一句 <<<----------------
return Boss[x] = Big_Boss; // 把 “沿途每个人(包括自己)的老板” 都设为 “最终找到的那个最大的老板”
}
}
● 那么上面那个 故事 就可以演示为:
四、完整代码
#include<stdio.h>
int a[10010];
int x, y, flag, N, M;
int Find_Big_Boss(int x)
{
if (a[x] == x) // 自己所属的老板是自己
return x;
else // 自己所属的老板不是自己
{
int Big_Boss = Find_Big_Boss(a[x]); // 通过递归去找到 自己的老板的 “所属的最大的老板”
return a[x] = Big_Boss; // 把 “沿途每个人(包括自己)的老板” 都设为 “最终找到的那个最大的老板”
}
}
int main()
{
scanf("%d%d", &N, &M);
for (int i = 1; i <= N; i++) // 初始化
{
a[i] = i; // 自己的老板是自己
}
while (M > 0)
{
scanf("%d%d%d", &flag, &x, &y);
int Big_Boss_x = Find_Big_Boss(x); // x 去找到自己的 “最大的老板”
int Big_Boss_y = Find_Big_Boss(y); // y 去找到自己的 “最大的老板”
if (flag == 1) // 集合合并操作
{
a[Big_Boss_y] = Big_Boss_x; // 把 y 的 “最大的老板” 的老板 变成 x 的 “最大的老板”
}
else if (flag == 2) // 集合查询操作
{
if (Big_Boss_x == Big_Boss_y) // x 的 “最大的老板” 和 y 的 “最大的老板” 一样
printf("Y\n"); // 就说明在同一个集合
else
printf("N\n"); // 否则不在同一个集合
}
M--;
}
return 0;
}
五、做题小结与反思
● 学一个东西,学一个算法,有时候一看好像就看懂了,但一上手实践就出 Bug。所以呀,还是要多实践啊。
六、参考附录
[1] 原题地址:https://www.luogu.com.cn/problem/P3367.
[2] 对于“并查集”讲得很不错的一篇知乎:《算法学习笔记(1) : 并查集》.
上一题链接: C/C++百题打卡[6/100]——表格涂色「MCOI-06」Gerrymandering.
百题打卡总目录: 🚧 🚧 …
C/C++百题打卡[7/100]——【模板】并查集 ⭐️ ⭐️ ⭐️
标签:并查集
不定期更新
2022/2/21