带权并查集
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。
A 吃 B,B 吃 C,C 吃 A。
现有 N 个动物,以 1∼N 编号。
每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y,表示 X 和 Y 是同类。
第二种说法是 2 X Y,表示 X 吃 Y。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
当前的话与前面的某些真的话冲突,就是假话;
当前的话中 X 或 Y 比 N 大,就是假话;
当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。输入格式
第一行是两个整数 N 和 K,以一个空格分隔。
以下 K 行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D 表示说法的种类。
若 D=1,则表示 X 和 Y 是同类。
若 D=2,则表示 X 吃 Y。输出格式
只有一个整数,表示假话的数目。数据范围
1≤N≤50000,
0≤K≤100000
输入样例:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出样例:
3 -
题目来源:https://www.acwing.com/problem/content/242/
题目分析:
- N个动物其实只有三类,A B C
- 多个动物构成的食物链结构可以采用链表和树的形式存储
- 在合并两个链表 或 树时,采用并查集的方式
- 下面我们来讲解本质是树的带权并查集
算法原理:
模板算法:
带权并查集:
1. 存储方式:
- fa[N],若 fa[x] = y,则表示y吃x
- d[N],记录节点在树/链表中的深度(链表是极端的树)
当一条食物链中节点i深度d[i]%3==0时,认为动物i是A类;
d[i]%3==1时,认为动物i是B类;d[i]%3==2时,认为动物i是C类。
简而言之:%后为0层吃%后为1层,%后为1层吃%后为2层,%后为2层吃%后为0层 - find(x),查找节点x所在食物链的链表头,或所在食物树的树根
2. 建立食物树:
1. x和y的关系是否可以明确:
-
当find(x) != find(y)时,说明x 和 y不在一条食物链或食物树上
此时的x y的状态如下:
-
虽然知道动物a吃y,y吃b
也知道d吃x
但是此时并不能确定a b y三者哪个是A类,哪个是B类动物,哪个是C类
更不知道d和x哪个是A类或B类或C类
所以无法知道x 和y的吃和被吃的关系 -
若此时给定 fa[x] = y,即y吃x,则可以将两条链表或者说两个树合并起来
现在我们让x所在链表去连接y所在链表:
-
将两个链表的链表头,或者说两个单枝树的树根相连后
此时我们可以判断a b c x y 任意两者的吃与被吃的关系了
我们知道合并后的树中所有节点的相对关系后,就不必分清ABC类了
2. 两树合并的方式:
- 如上图所示我们合并两个树或者说链表时,仅仅将树根/表头连接即可
- 连接后,主动连接者的深度发生变化,而被动连接者的各节点深度未发生变化
- 具体变化规则为:
主动连接方的树根深度d[find(x)] 原本为0
加了原本y节点深度d[y]-原本x节点深度d[x]+k
其中当y吃x时,k = 1;当y和x同类时,k = 0;
总结为:
d[find(被吃)] += d[吃] - d[被吃] + 1;
d[find(同类1)] += d[同类2] - d[同类1];
3. 判断关系是否正确:
- 题干中有这样一句话:当前的话与前面的某些真的话冲突,就是假话;
- 也就是说,最早出现的x y的关系肯定是正确的
- 则我们只需要判断第k句描述x y关系的话,k>=2
1. 判断x y是否可以比较:
- find(x) == find(y),则x y所在的树已完成了合并,可以比较,现在只需要判断
- find(x) != find(y),则x y所在的树尚未完成合并,这句描述xy关系的话就是第一句,现在进行x y的合并
2. 判断x y的关系:
- 若指令为 1 x y,则查看 (d[x] - d[y]) %3 是否为0
- 若指令为 2 x y,则查看 (d[x] - d[y] - 1) % 3是否为0
写作步骤:
1. find()函数:
- 通过上面的描述,我们知道了find()函数用来溯源
- 由于存在d[]数组描述树中每个节点的深度,所以find()函数还兼任着修正节点深度的功能
2. 首次描述关系用于建树:
- 首次描述前,两节点不连通,find(x) != find(y)
- 将一个树连接到另一个树上
- 修正主动连接的树根的深度为 d[x]- d[y] + 1/0;
3. 非首次描述关系用于判断:
- 非首次描述,则两个节点连通,find(x) == find(y)
- 直接比较 (d[x]-d[y])%3 == 0,或者 (d[x]-d[y]+1)%3 == 0
- 不要以为 % 优先级低就可以不加括号,我为此付出了30分钟debug
代码实现:
#include <iostream>
using namespace std;
const int N = 50010;
int fa[N];
int d[N];
int n;
void init(){
for(int i=1; i<=n; i++) fa[i] = i, d[i] = 0;
}
int find(int x){
if(fa[x] == x) return x;
int up = find(fa[x]);
d[x] += d[fa[x]]; //d[x]是x到fa[x]的举例,d[fa[x]]是fa[x]到新树根的举例
fa[x] = up;
return fa[x];
}
int main(){
int m = 0;
cin >>n >>m;
init();
int res = 0;
for(int i=0; i<m; i++){
int op = 0, x = 0, y = 0;
cin >>op >>x >>y;
if (x<=0 || x>n || y<=0 || y>n) {
res++;
continue;
}
int fax = find(x), fay = find(y);
if (op == 1){
if (fax != fay){
fa[fay] = fax; //y连接到x上,由于此处xy同级别,所以此处也可以x连接到y上
d[fay] = d[x] - d[y];
}else if((d[y]-d[x])%3) res++;
}else{
if (fax != fay){
fa[fay] = fax; //只能被吃的y连接到吃人的x上,顺应着y树的深度下降,下面是d[y]-d[x]
d[fay] = d[x] - d[y] + 1;
}else if((d[y]-d[x]-1)%3) res++;
}
}
cout<< res <<endl;
return 0;
}
代码误区:
0. 为什么d[N]初始化为0?
- 构建食物链构成的食物树时,我们将树根或链头的深度设为0
- 初始每个点都看作一个独立的树,这个节点就是树根,深度当然为0
1. find()函数的理解:
- find()本身的目的不变,就是返回链表头或树根
- 只有树根发生变化的时候find()函数发生对树的修改
一方面修正每个链路上点的fa[]为新树根
另一方面修正每个链路上点到新树根的距离 - 使用临时变量up暂存新树根find(fa[x])
则fa[x]本身还是x的原树根
由于find(fa[x])的完成,d[fa[x]]从0更正为老树根到新树根的距离
d[x] = 老树根到新树根的距离 + x到老树根的距离
完成d[x]的更新后再将fa[x]修正为新树根
2. 合并树时的深度修改:
-
当 x y同层时,x所在的树连接入y所在的树和y所在的树连接到x所在的树都可
-
当x 吃 y时,最终x在y的食物链靠近链头,x深度较小,即(d[y]-d[x]+1)%3==0,
所以最好此时将被吃的y所在的树插入吃人的x所在的树,修正y所在树的深度即可
3. 逻辑上的树:
- find()函数本身还是直接将所有食物链上的点的fa[]直接改为链头,是中心结构
- 单单依靠find()和fa[]还是建立的中心围绕点的集合,不是树
- d[]和fa[],一个描述节点在食物链的深度,一个描述链头是谁,两个一起打造了逻辑上的树
本篇感想:
- 本篇有些难度,我想一个最简单的方式讲解花费了4小时,太真实辣!!!
- 带权并查集其实使用的是树和链表
- 理解了三点之后就掌握了带权并查集:
- 深度%3==0者 吃 深度%3==1者
深度%3==1者吃深度%3==2者
深度%3==2者 吃 深度%3 == 0者 - 比较两个点关系前,使用find(x) == find(y)查看两者是不是位于同一树下,处于同一树下的才可以使用d[N]查看关系
- 不属于同一树下的需要合并树,将树根合并,同时更新一方节点深度
- 深度%3==0者 吃 深度%3==1者
- 看完本篇博客,恭喜已登 《练气境-后期》
距离登仙境不远了,加油