题目一:合并集合
分析
暴力做法:需要维护 两个索引,例如维护编号i = 2 和 i = 4 的索引,如果索引不相等,修改索引,非常耗时。这样需要将很多个数据的索引都改成 某个编号。比如:如果1000个数据的根都是编号2,此时我想要接在根编号3上。那么就会出现,修改其中一个数据的根,就要修改1000个数据的根为3.
并查集思想呢。就是树,只要将树根接到其他的树根就好。
并查集的路径压缩优化:在找某编号的根的同时,找到根了,路径上所有编号的父母索引都修改为根编号。
其实就是玩索引玩数组,编号以及编号的值,编号的值就是需要索引联系到的地方。例如p[2] = 3; 从编号2的父母值3,我可以通过3联系到编号3的相关信息(也就是他的p[3] )
代码 (路径压缩)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int n, m;
int p[N];
// 返回x的根 + 路径压缩
// 通过递归,不断找到x的根, 同时,找到根,回退,使所有路径的p 都等于 根
// 没有路径压缩会tle ,纯找根
int find (int x) {
if(p[x]==x) return p[x]; // 返回x的根
p[x] = find(p[x]); // 找根的同时,不断更新路径上的p[x];
}
// 纯找根
int find1(int x) {
if(p[x]==x) return x;
x = find1(p[x]); // 不断递归x的父母p[x], 为了不断往上找
}
int main() {
cin >> n >> m ;
for(int i = 1; i <= n; i ++) p[i] = i;
while(m --) {
char op;
int a, b;
cin >> op >> a >> b;
// 合并
if(op == 'M') p[find(a)] = find(b); // a根结点 接到b根上
else {
if(find(a) == find(b) ) puts("Yes");
else puts("No");
}
}
return 0;
}
题目二:连通块中点的数量
分析
连通块,相当于集合。其实就是维护集合,集合属性两个,一是编号,二是大小(点数)
连通就是集合合并,判断是否在一个连通块就是找根比较,点数就是size
编号在索引数组体现。
需要开一个维护集合大小的数组 size
注意的是,a,b可能在同一个连通块已连通,此时直接continue不用连通了。
代码
#include<bits/stdc++.h>
using namespace std;
int n, m;
const int N = 1e5+10;
int p[N], Size[N];
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; Size[i] = 1;
}
while(m --) {
string op;
int a, b;
cin >> op ;
if(op=="C") {
cin >> a >> b;
if(find(a)==find(b)) continue;
Size[find(b)] += Size[find(a)];
p[find(a)] = find(b);
}
else if(op=="Q1") {
cin >> a >> b;
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
else {
cin >> a;
cout << Size[find(a)] << endl;
}
}
return 0;
}
题目三:食物链
分析
这道题的思路就是,合并成同一集合:什么意思呢?
当题目给出两编号有关系时,将他们合并,同时维护他们到根节点的距离,d[x]
如果距离差模3为0,说明是同类,如果距离差模3为1,说明前者吃后者。
为什么模3呢?
因为A、B、C共三类,并且是循环,通过模3的数值来判断关系。
通过,他们与根c的关系就能知道他们的关系: 例如A与B
b与c的距离1,a与c距离1+1,所以a-b距离差模3是1,所以a吃b。
例如: 空 与 c,c与c距离0,空与c距离3.所以距离差模3 == 0;是同类。
所以,只要维护他们与根距离就能判断他们的关系:d[x]
- 距离差模3等于 1 : 前者吃后者
- 距离差模3等于 0 : 同类
- 需要模3,距离实际上可以是11,10,啥的,关系是循环,与根节点距离模3代表关系。
find(x) => 路径压缩+加维护d[x]
先把关系维护了,再压缩(更新p[x],指向根结点)
先更新好了,再断关系,关系断了,就无法更新d[x] += d[p[x]] 距离更新了
至于两个的距离合并,一个是距离差模3为0,和距离差模3为1,举个模3为1例子。
如果,d = 2,x 吃 y , 那么 合并集合后 x与根距离,和y与根距离,距离差模3为1.
但是现在还没合并集合,需要让我来合并集合:
需要更新d[dx],指向之后更新距离:
总结一下,各自属于不同集合,有关系则集合合并,通过两点距离差模3的关系,判断是循环中哪种关系。在压缩路径的同时,提前更新d[x]。
A->B->C->A
循环呢,就两种关系,吃,和同类的关系。 距离1,前者吃后者,后者被吃,距离0,同类。
A与C的关系虽然是距离差2,但其实,是C->A,距离1。毕竟是循环。
代码
#include<bits/stdc++.h>
using namespace std;
int n, m;
const int N = 1e5+10;
int p[N], d[N];//索引 和 维护到根结点距离
int find(int x) {
if(p[x]!=x) {
// 不是根
int t = find(p[x]); // 将最终返回根结点保存起来
d[x] += d[p[x]]; // 逐一更新到根节点距离
p[x] = t; //指向根节点,路径压缩
}
// 存根的t赋值给p[x],返回根,目的是路径压缩
return p[x];
}
// 这样写不行 , 原因是没有了return ,要先不断找,最后不断return 值
int find1(int x) {
if(p[x] == x) return p[x];
int t = find1(p[x]);
d[x] += d[p[x]];
p[x] = t;
}
int res = 0;
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i; // 索引//集合
while(m --) {
int t, x, y;
cin >> t >> x >> y;
if(x > n || y > n) res ++;
else {
int px = find(x), py = find(y); // 找根
if(t == 1) {
//根相同,同一集合但是距离差模3不为0,假的,应该为0
if(px==py && (d[x]-d[y])%3) res ++;
// 根不同,合并集合
else if(px!=py) {
p[px] = py; // 合并
d[px] = d[y] - d[x]; // 更新合并后px->py 的那一小段距离,模后为0;
}
}
else {
// 根相同,同一集合但是距离差模3不等于1,假的,应该为1
if(px==py && (d[x]-d[y]-1)%3) res ++;
// 跟不同,合并集合
else if(px!=py) {
p[px] = py;
d[px] = d[y] + 1 - d[x];//同理,只是模后=1;
}
}
}
}
cout << res << endl;
return 0;
}