带权并查集
内容:
我们可以在并查集的边上定义某种权值、以及这种权值在路径压缩时产生的运算,从而解决更多的问题
操作
用父亲节点记录子树的权值,显而易见根节点就记录树的权值
inline int Find(int x)
{
if(x==fa[x]) return x;
int fx=fa[x];
fa[x]=Find(fx);
dis[x]+=dis[fx];
return fa[x];
}//dis[x]为x节点到根节点距离
void merge(int x,int y,int s)
{
fx=Find(x),fy=Find(y);
if(fx!=fy)
{
fa[fx]=fy;
dis[fx]=dis[y]-dis[x]+s;
}
}
(每个题的更新dis的方法会有所不同,视题意而定)
例题
带权并查集例题真的难找 \(qwq\)
1.洛谷P1196 [NOI2002] 银河英雄传说
题目背景
略~~~
输入格式
第一行有一个整数T(1<=T<=5e5),表示有T条指令。
接下来T行,每行一条指令,两种
1.M i j (i,j为正整数且<=30000) 表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第i号战舰与第j号战舰不在同一列
2.C i j(i,j为正整数且<=30000)表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令.
输出格式
依次对输入的每一条指令进行分析和处理:
如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息。
如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第i号战舰与第 j号战舰之间布置的战舰数目。如果第 i号战舰与第 j 号战舰当前不在同一列上,则输出 −1。
分析:
合并操作,直接进行并查集合并即可,但询问操作 显然 用普通并查集无法实现 , 所以我们采用带权并查集。每个点不仅记录他的祖宗还要记录他的深度,然后进行路径压缩
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std ;
struct StarMaster
{
private :
int n , m , deep[300010] , fa[300010] , sum[300010] ;
inline int read()
{
int x = 0 , f = 1 ;
char ch = getchar() ;
while(ch > '9' or ch < '0')
{
if(ch == '-') f = -1 ;
ch = getchar() ;
}
while(ch >= '0' and ch <= '9')
{
x = (x << 1) + (x << 3) + (ch^48) ;
ch = getchar() ;
}
return x * f ;
}
inline void Init()
{
for(int i = 1 ; i <= 30000 ; i ++)
{
fa[i] = i ;
sum[i] = 1 ;
}
}
inline int Find(int x)
{
if(x == fa[x]) return x ;//递归边界
else
{
int tmp = fa[x] ;//记录下来 fa[x] , 便于递归
fa[x] = Find(fa[x]) ;//进行下推 保证 deep[fa[x]] 已被更新
deep[x] += deep[tmp] ;//更新 x 的深度将 x 所在队列根节点 fx 接到 y 的后面 , 所以 x 的深度为他在原队列的深度加上他父亲结点的深度(相当于一个递归的过程从 x 推到相接的 fx 再回推更新 deep[fx]' = deep[fx] + deep[y] ,推到对列的末端
return fa[x] ;
}
}
inline int abs(int x)
{
return x > 0 ? x : -x ;
}
public :
inline void ioi()
{
m = read() ;
Init();
for(int i = 1 ; i <= m ;i ++)
{
char opt ;
cin >> opt ;
if(opt == 'M')
{
int x = read() , y = read();
int fx = Find(x) , fy = Find(y) ;
fa[fx] = fy ;
deep[fx] = sum[fy] ;
sum[fy] += sum[fx] ;
} //sum用于计算各点所在队列末端到对首的总深度。
if(opt == 'C')
{
int x = read() , y = read() ;
int fx = Find(x) , fy = Find(y) ;
if(fx == fy)
{
cout << abs(deep[x] - deep[y]) - 1 <<'\n';
}
else
{
cout << -1 <<'\n' ;
}
}
}
}
}ak;
int main()
{
ak.ioi() ;
return 0 ;
}
2.P2024 [NOI2001] 食物链
题目描述
动物王国中有三类动物 $ 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\),表示有 \(N\) 个动物,\(K\) 句话。
第二行开始每行一句话(按照题目要求,见样例)
输出格式
一行,一个整数,表示假话的总数。
分析 :
两种方法:
方法一
先说一个巧妙的方法 (大部分人最先想到的方法):种类并查集
fa[] 开三倍 用于 分别存储三种关系 来进行操作 。
\(1···n\) 存储同类 , \(n+1 ··· 2n\) 存储猎物 ,\(2n + 1···3n\) 存储猎杀者
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std ;
struct StarMaster
{
private :
int n , m , fa[200010] , ans = 0;
inline int read()
{
int x = 0 , f = 1 ;
char ch = getchar() ;
while(ch > '9' or ch < '0')
{
if(ch == '-') f = -1 ;
ch = getchar() ;
}
while(ch >= '0' and ch <= '9')
{
x = (x << 1) + (x << 3) + (ch^48) ;
ch = getchar() ;
}
return x * f ;
}
inline int Find(int x)
{
if(fa[x] == x) return x ;
return fa[x] = Find(fa[x]) ;
}
inline void Init()
{
for(int i = 1 ; i <= 3 * n ; i ++)
{
fa[i] = i ;
}
}
inline int abs(int x)
{
return x > 0 ? x : -x ;
}
public :
inline void ioi()
{
n = read() , m = read() ;//1 ~ n 存储同类 , n+1 ~ 2n 存储猎物 ,2n+1 ~ 3n 存储猎杀者
Init() ;
for(int i = 1 ; i <= m ; i ++)
{
int opt = read() , x = read() , y = read() ;
if( x > n or y > n)
{
ans ++ ;
continue ;
}//最离谱的假话直接记录;
if(opt == 1)
{
if(Find(x + n) == Find(y) or Find(x + 2 * n) == Find(y))
{
ans ++ ;
continue ;
} //如果 y 是 x 的猎物或猎杀者 , 那么显然 x 和 y 必然不是同类
else
{
fa[Find(x)] = Find(y) ;
fa[Find(x + n)] = Find(y + n) ;
fa[Find(x + n * 2)] = Find(y + n * 2) ;
} // 同类所有都一样
}
else
{
if ( x == y or Find(x + 2 * n) == Find(y) or Find(x) == Find(y))
{
ans ++ ;
continue ;
}
else
{
fa[Find(x)] = Find(y + 2 * n) ;// x 与 y + 2n 并 说明 x 是 y 的 捕杀者
fa[Find(x + n)] = Find(y) ;//x + n 与 y 并表示 y 是 x 的猎物
fa[Find(x + 2 * n)] = Find(y + n) ;//x + 2n 与 y + n 并表示 y 的猎物 是 x 的捕杀者(题目中已说明总共 3 种动物捕食关系形成一个环)
}
}
}
cout << ans << '\n' ;
}
}ak;
int main()
{
ak.ioi() ;
return 0 ;
}
方法二
第二种方法(真 · 解):用权值表示关系 。
据题意,森林中有 $ 3 $ 种动物:\(A\) 吃 \(B\) ,\(B\) 吃 \(C\) ,\(C\) 吃 \(A\) 。
使用带权并查集,我们就以动物之间的关系来作为并查集每个结点的权值。
注意,我们不知道所给的动物(题目说了,输入只给编号)所属的种类。所以,我们可以用动物之间的“相对关系”来确定一个并查集
我们用 $ rs[] $ 表示关系(\(relationship\) )\(rs[x]=0\) 表示 这个节点 \(x\) 与 \(fa[x]\) 是同类 , \(rs[x]=1\) 表示 \(x\) 是 \(fa[x]\) 的猎物, \(rs[x]=2\) 表示 \(x\) 是 \(fa[x]\) 的捕杀者,每次合并后对 \(3\) 进行取模 。(初始 $ rs[x] = 0 $ 自己和自己是同类 ) 。
推算 :
x 吃 y 共根 表示 :
(1) \(rs[x] = 2\) , \(rs[y] = 0\) (根节点是 \(x\) 的猎物 , \(y\) 与根节点是同类,所以 \(x\) 吃 \(y\) )
(2) \(rs[x] = 1\) , \(rs[x] = 2\) ( 根节点是 \(x\) 的捕杀者 ,根节点是 \(y\) 的猎物 , 所以 \(x\) 吃 \(y\) ) ;
(3) \(rs[x] = 0\) , \(rs[y] = 1\) (根节点和 \(x\) 是同类 , 根节点是 \(y\) 的捕杀者 , 所以 \(x\) 吃 \(y\));
共上述三种情况 , 归纳 : $ x $ 吃 y 可表示为 $ rs[y] = (rs[x] + 1)%3 $
我们可以用向量加减法推导更新权值的通式 :
种类并查集
内容:
一般的并查集,维护的是具有连通性、传递性的关系,例如亲戚的亲戚是亲戚。但是,有时候,我们要维护另一种关系:敌人的敌人是朋友。种类并查集就是为了解决这个问题而诞生的。
操作 :
目前有两种主流 :
一个是扩大 \(fa[x]\) 数组 加入一些判断
第二个是 利用带权并查集的思想 , 进行种类划分与判断
其实就是带权并查集