[模板] 并查集 - 带权并查集 (洛谷 P3367 银河英雄传说)

P1196 [NOI2002]银河英雄传说

带权并查集

题目背景

公元5801年,地球居民迁至金牛座α第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。

宇宙历799年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。

题目描述

杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成30000列,每列依次编号为1,2,···,30000。之后,他把自己的战舰也依次编号为1,2···,30000,让第i号战舰处于第i列,形成“一字长蛇阵”,诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为M i j,含义为第i号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第j号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。

然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。

在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第i号战舰与第j号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。

作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。

题目大意

现有n个元素排成n列,即每个元素自称成一列,有两种操作:

  • M i j:表示将i队列的头接至j队列的尾,合并指令的执行结果会使i队列增大,j队列的元素数量成为0

  • C i j:询问ij是否在同一个队列中,若是,输出ij之间的元素数量,否则输出-1

输入格式

第一行有一个整数T (1≤T≤5×1E5),表示总共有T条指令。

以下有T行,每行有一条指令。指令有两种格式:

  • M i jij是两个整数(1≤i,j≤30000),表示指令涉及的编号。该指令是合并指令,并且保证i元素与j元素不在同一列。

  • C i jij是两个整数(1≤i,j≤30000),表示指令涉及的编号。该指令是询问指令。

输出格式

对于询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,i号与j号元素之间的元素数量。如果i号与j号元素当前不在同一列上,则输出 −1

输入输出样例

输入输出
4
M 2 3
C 1 2
M 2 4
C 4 2
-1
1

说明/提示

战舰位置图:表格中阿拉伯数字表示战舰编号

思路

带权并查集实现。在一般并查集基础上增加dep记录所在集的位置上的深度(模拟链表),在队列头记录该集下的元素数量,同时带有路径压缩优化实现。

代码

//cpp
#include <iostream>
#include <cmath> //abs

using ::std::cin;
using ::std::cout;

struct ufsets_elem_w{
	int dep, vn;		 //dep表示元素在队列上的位置,vn=0表示不是队列头,vn有效时表示这个元素是队列头,值等于这个队列上的元素数量。
	ufsets_elem_w *root; //root表示该元素所在队列的第一个元素
	ufsets_elem_w(){
		dep = 0, vn = 1;
		root = this;
	}
	ufsets_elem_w *find(){
		if (this != root){					   //该节点不是根节点
			ufsets_elem_w *tmp = root->find(); //带路径压缩的根节点记录(缓存了更新后的根节点值)
			dep += root->dep;				   //该节点深度+=更新前的根节点深度
			root = tmp;						   //更新根节点
		}
		return root;
	}
};

struct ufsets_w{				//带权并查集
protected:
	int ufsets_num;				//独立集合数量
	ufsets_elem_w *ufsets_base; //并查集数组
public:
	explicit ufsets_w(const int &n){
		ufsets_base = new ufsets_elem_w[n + 1]();
		ufsets_num = n;
	}
	bool catenate(const int &a, const int &b){	   //合并a,b集合,返回false则说明两元素已是同一集合
		ufsets_elem_w *rb = ufsets_base[b].find(); //路径压缩b节点并临时缓存(下一步会更改root,必须缓存)
		if (ufsets_base[a].find() != rb){		//路径压缩a节点,若a,b不在同一集合则合并
			--ufsets_num;
			rb->dep += ufsets_base[a].root->vn; //b所在队首深度更新至this队尾
			rb->root = ufsets_base[a].root;		//b所在队首根更新为this的根
			ufsets_base[a].root->vn += rb->vn;	//a所在队首记录的元素数量+=b队列的数量
			rb->vn = 0;							//取消b队列的队列头状态
			return true;
		}
		return false;
	}
	bool relative(const int &a, const int &b){ //查询是否在同一个集合中,是则返回true
		return ufsets_base[a].find() == ufsets_base[b].find();
	};
	int depth(const int &pos){//元素位于所在队列的哪一个位置
		return ufsets_base[pos].dep;
	}
	int size(){//独立集合数量
		return ufsets_num;
	}
	~ufsets_w(){
		delete[] ufsets_base;
	}
};

const int NMAX = 30000 + 5;

int main(){
	int t;
	char tmp1;
	int tmp2, tmp3;
	::std::ios::sync_with_stdio(false);
	cin >> t;
	ufsets_w u_w(NMAX);
	for (int i = 1; i <= t; ++i){
		cin >> tmp1 >> tmp2 >> tmp3;
		switch (tmp1){
		case 'M':
			u_w.catenate(tmp3, tmp2); //把tmp2的队列移动到tmp3所在队列的后面
			break;
		case 'C':
			if (!u_w.relative(tmp2, tmp3)) //tmp2,tmp3不在同一集合中
				cout << "-1\n";
			else
				cout << abs(u_w.depth(tmp2) - u_w.depth(tmp3)) - 1 << '\n'; //中间相隔的元素数量,需要-1
			break;
		}
	}
	return 0;
}
//java
import java.util.*;
import java.math.*;

class ufsets_elem_w{
	int dep, vn;		 //dep表示元素在队列上的位置,vn=0表示不是队列头,vn有效时表示这个元素是队列头,值等于这个队列上的元素数量。
	ufsets_elem_w root; //root表示该元素所在队列的第一个元素
	ufsets_elem_w(){
		dep = 0;
		vn = 1;
		root = this;
	}
	ufsets_elem_w find(){
		if (this != root){					   //该节点不是根节点
			ufsets_elem_w tmp = root.find(); //带路径压缩的根节点记录(缓存了更新后的根节点值)
			dep += root.dep;				   //该节点深度+=更新前的根节点深度
			root = tmp;						   //更新根节点
		}
		return root;
	}
};

class ufsets_w{				//带权并查集
	int ufsets_num;				//独立集合数量
	ufsets_elem_w ufsets_base[]; //并查集数组
	ufsets_w(final int n){
		ufsets_num = n;
		ufsets_base = new ufsets_elem_w[n + 1];
		for(int i=1;i<=n;i++)//ufsets_base[0]用不到
			ufsets_base[i]=new ufsets_elem_w();
		ufsets_num = n;
	}
	boolean catenate(final int a, final int b){	   //合并a,b集合,返回false则说明两元素已是同一集合
		ufsets_elem_w rb = ufsets_base[b].find(); //路径压缩b节点并临时缓存(下一步会更改root,必须缓存)
		if (ufsets_base[a].find() != rb){		//路径压缩a节点,若a,b不在同一集合则合并
			--ufsets_num;
			rb.dep += ufsets_base[a].root.vn; //b所在队首深度更新至this队尾
			rb.root = ufsets_base[a].root;		//b所在队首根更新为this的根
			ufsets_base[a].root.vn += rb.vn;	//a所在队首记录的元素数量+=b队列的数量
			rb.vn = 0;							//取消b队列的队列头状态
			return true;
		}
		return false;
	}
	boolean relative(final int a, final int b){ //查询是否在同一个集合中,是则返回true
		return ufsets_base[a].find() == ufsets_base[b].find();
	}
	int depth(final int pos){//元素位于所在队列的哪一个位置
		return ufsets_base[pos].dep;
	}
	void refresh(final int end) {//针对java的特殊优化,刷新各节点存储的root值(防止StackFlowError)
		for(int i=1;i<=end;i++)
			ufsets_base[i].find();
	}
	int size(){//独立集合数量
		return ufsets_num;
	}
};


public class Main {
	
	final static int MAXN=30000;

	public static void main(String[] args) {
		Scanner sc=new Scanner(System.in);
		int t=sc.nextInt();
		int max_use=0,m_tm=0;//针对java的特殊优化
		ufsets_w u_w=new ufsets_w(MAXN);
		for (int i = 0; i <= t; ++i){
			String s[]=sc.nextLine().split(" ");
			switch (s[0]){
			case "M":
				{//针对java的特殊优化,刷新各节点存储的root值(防止StackFlowError)
					m_tm++;
					max_use=Math.max(Math.max(Integer.valueOf(s[2]),Integer.valueOf(s[1])),max_use);
					if(m_tm%173==0){
						u_w.refresh(max_use);
					}
				}
				u_w.catenate(Integer.valueOf(s[2]), Integer.valueOf(s[1])); //把s[1]的队列移动到s[2]所在队列的后面
				break;
			case "C":
				if (!u_w.relative(Integer.valueOf(s[1]), Integer.valueOf(s[2]))) //不在同一集合中
					System.out.println("-1");
				else
					System.out.println(Math.abs(u_w.depth(Integer.valueOf(s[1])) - u_w.depth(Integer.valueOf(s[2]))) - 1); //中间相隔的元素数量,需要-1
				break;
			}
		}
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
并查集维护到祖宗距离是指在并查集的数据结构中,除了记录每个元素的父节点外,还记录了每个元素到其祖宗节点的距离。这样可以在查找祖宗节点的同时,更新每个元素到祖宗节点的距离。这个功能可以通过路径压缩来实现。路径压缩是指在查找祖宗节点的过程中,将路径上的每个节点的父节点直接指向祖宗节点,从而减少后续查找的时间复杂度。在路径压缩的过程中,还可以同时更新每个节点到祖宗节点的距离。这样,在后续的操作中,可以直接通过查找元素的祖宗节点来获取元素到祖宗节点的距离,而无需再次进行计算。引用\[1\]中的代码是一个实现路径压缩的示例,其中的d数组就是用来记录每个元素到祖宗节点的距离。引用\[2\]和引用\[3\]中也提到了路径压缩的概念和实现方式。 #### 引用[.reference_title] - *1* [AcWing 238. 银河英雄传说--(维护size和到祖宗节点距离)带权并查集](https://blog.csdn.net/qq_45748404/article/details/117967117)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [算法基础课—数据结构(六) 并查集](https://blog.csdn.net/Yttttttttttttttt/article/details/117067020)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值