带权并查集

带权并查集

内容:

我们可以在并查集的边上定义某种权值、以及这种权值在路径压缩时产生的运算,从而解决更多的问题

操作

用父亲节点记录子树的权值,显而易见根节点就记录树的权值

  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]\) 数组 加入一些判断

第二个是 利用带权并查集的思想 , 进行种类划分与判断

其实就是带权并查集

例题:

1.P2024 [NOI2001] 食物链

上一篇:goland fx 依赖注入


下一篇:Java仿haskell