带权并查集

更全的内容详见并查集全面讲解

带权并查集

内容:

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

操作

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

  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 $


我们可以用向量加减法推导更新权值的通式 :

我们可以用向量加减法推导更新权值的通式 :

\[rs[fa[x]] = 0 \\ rs[x] = 1\\ rs`[x] = 1\\ rs`[x] = (rs[fa[x]] + rs[x])~\%~3\\ ***\\ rs[fa[x]] = 1 \\ rs[x] = 1 \\ rs`[x] = 2\\ rs`[x] = (rs[fa[x]] + rs[x])~\%~3\\ \dots \]

经过推算我们发现 \(rs[x]\) 的合并(路径压缩)符合向量加减法运算


下面我们来推算两个不同根的合并

1628467028861

假设我们要合并 \(x\)\(y\)

我们有输入的 \(d\) ( \(x\)\(y\) 的关系) , 转化为我们所用为 \(d - 1\) 所以由向量加减法得

\[rs`[y] = (d-1 + rs[x])~\%~3\\ rs`[fa[y]] = (ra`[y]-rs[y] + 3)~\%~3\\ \]

检验

\[rs[x]=0\\ d-1=2\\ rs`[y]=2\\ rs[y]=1\\ rs`[fa[y]]=1\\ **\\ rs`[fa[y]]=(rs`[y]-rs[y]+3)~\%~3 \]

检验成立

判断合不合法 :

我们首先观察条件2和条件3 :

2 :当前的话中 X 或 Y 比 N 大,就是假话

3 :当前的话表示 X 吃 X,就是假话

十分智障 直接简单的 \(if\) 解决

再看条件1

1 :当前的话与前面的某些真的话冲突,就是假话

我们已推出关系转化符合向量加减法 , 所以我们只需判断在同根的情况下此条件能否满足向量加减法即可( 因为不同根情况下必不会产生冲突 )

\[because :\\ ~~~rs[y]=(d-1+rs[x])~\%~3\\ so :\\ ~~~d-1=(rs[y]-rs[x]+3)~\%~3 \]

#include<bits/stdc++.h>

using namespace std ;

class StarMaster
{
    private :
        int n , k , ans , fa[100010] , rs[100010] ;
    	inline int read()
        {
            int x = 0 , f = 1 ;
            char ch = getchar() ;
            while(ch < '0' or ch > '9')
            {
                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 ;
            else
            {
                int tmp = fa[x] ;
             	fa[x] = Find(fa[x]) ;
                rs[x] = (rs[tmp] + rs[x]) % 3 ;
                return fa[x] ;
		    }
        }
        inline void merge(int x , int y , int d)
        {
            int fx = Find(x) , fy = Find(y) ;
            if(fx == fy) return ;
            else
            {
                fa[fy] = fx ;
                rs[fy] = (d - 1 + rs[x] - rs[y] + 3) % 3 ;   
			}
            return ;
        }
        inline void Init()
        {
        	for(int i = 1 ; i <= n ; i ++)
            {
                fa[i] = i ;
                rs[i] = 0 ;
            }
        }
    public :
        inline void ioi()
        {
            n = read() , k = read() ;
            Init() ;
            for(int i = 1 ; i <= k ; i ++)
            {
                int d = read() , x = read() , y = read() ;
                if((x > n or y > n) or (d == 2 and x == y) )
                {
                    ans ++ ;
                    continue ;
                }
                int fx = Find(x) , fy = Find(y) ;
                if(fx == fy)
                {
                    if((d - 1)!=(rs[y] - rs[x] + 3) % 3)
                    {
                        ans ++ ;
                        continue ;
                    }
                }
                else
                {
                    merge(x , y , d) ;
                }
            }
            cout << ans << '\n' ;
        }
}ak;

int main()
{
	ak.ioi() ;
	return 0 ;
}
posted @ 2021-08-08 19:14  theStarMaster  阅读(81)  评论(2)    收藏  举报