P1196 [NOI2002] 银河英雄传说
题目背景
公元 580158015801 年,地球居民迁至金牛座 α\alphaα 第二行星,在那里发表银河联邦创立宣言,同年改元为宇宙历元年,并开始向银河系深处拓展。
宇宙历 799799799 年,银河系的两大军事集团在巴米利恩星域爆发战争。泰山压顶集团派宇宙舰队司令莱因哈特率领十万余艘战舰出征,气吞山河集团点名将杨威利组织麾下三万艘战舰迎敌。
题目描述
杨威利擅长排兵布阵,巧妙运用各种战术屡次以少胜多,难免恣生骄气。在这次决战中,他将巴米利恩星域战场划分成 300003000030000 列,每列依次编号为 1,2,...,300001, 2,\ldots ,300001,2,...,30000。之后,他把自己的战舰也依次编号为 1,2,...,300001, 2, \ldots , 300001,2,...,30000,让第 iii 号战舰处于第 iii 列,形成"一字长蛇阵",诱敌深入。这是初始阵形。当进犯之敌到达时,杨威利会多次发布合并指令,将大部分战舰集中在某几列上,实施密集攻击。合并指令为 M i j,含义为第 iii 号战舰所在的整个战舰队列,作为一个整体(头在前尾在后)接至第 jjj 号战舰所在的战舰队列的尾部。显然战舰队列是由处于同一列的一个或多个战舰组成的。合并指令的执行结果会使队列增大。
然而,老谋深算的莱因哈特早已在战略上取得了主动。在交战中,他可以通过庞大的情报网络随时监听杨威利的舰队调动指令。
在杨威利发布指令调动舰队的同时,莱因哈特为了及时了解当前杨威利的战舰分布情况,也会发出一些询问指令:C i j。该指令意思是,询问电脑,杨威利的第 iii 号战舰与第 jjj 号战舰当前是否在同一列中,如果在同一列中,那么它们之间布置有多少战舰。
作为一个资深的高级程序设计员,你被要求编写程序分析杨威利的指令,以及回答莱因哈特的询问。
最终的决战已经展开,银河的历史又翻过了一页......
输入格式
第一行有一个整数 TTT(1≤T≤5×1051 \le T \le 5 \times 10^51≤T≤5×105),表示总共有 TTT 条指令。
以下有 TTT 行,每行有一条指令。指令有两种格式:
-
M i j:iii 和 jjj 是两个整数(1≤i,j≤300001 \le i,j \le 300001≤i,j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特窃听到的杨威利发布的舰队调动指令,并且保证第 iii 号战舰与第 jjj 号战舰不在同一列。 -
C i j:iii 和 jjj 是两个整数(1≤i,j≤300001 \le i,j \le 300001≤i,j≤30000),表示指令涉及的战舰编号。该指令是莱因哈特发布的询问指令。
每条指令中都保证 i≠ji\neq ji=j。
输出格式
依次对输入的每一条指令进行分析和处理:
- 如果是杨威利发布的舰队调动指令,则表示舰队排列发生了变化,你的程序要注意到这一点,但是不要输出任何信息。
- 如果是莱因哈特发布的询问指令,你的程序要输出一行,仅包含一个整数,表示在同一列上,第 iii 号战舰与第 jjj 号战舰之间布置的战舰数目。如果第 iii 号战舰与第 jjj 号战舰当前不在同一列上,则输出 −1-1−1。
输入输出样例 #1
输入 #1
4
M 2 3
C 1 2
M 2 4
C 4 2
输出 #1
-1
1
说明/提示
样例解释
战舰位置图:表格中阿拉伯数字表示战舰编号。

代码

cpp
#include <bits/stdc++.h>
using namespace std;
const int N=3e4+5;
/*
压缩路径的并查集本质是树形结构,路径压缩的递归过程堪称精妙:
查询根节点的同时,回溯更新路径上所有父辈节点的根为最终根,
========== 核心关键 ==========
无论题意描述多复杂,我们**完全不需要遍历任何队列/集合元素**,
不需要显性维护队列结构、不需要合并队列元素、不需要遍历根的子节点,
仅靠三个核心参数就能精准维系战舰间的所有关联关系:
f[x]:x的父节点(递归更新后最终指向根)
h[x]:x到根节点的路径上的元素总数(路径长度)
amount[x]:以x为根的集合的元素总数(集合大小)
这三个参数足以解决所有核心问题,多余的遍历操作完全是画蛇添足!
*/
struct node{// 并查集核心节点结构,仅需三个参数,无需任何遍历操作
int f, // 父节点(递归更新后为根节点)
h, // 当前节点到根节点的路径元素总数(路径长度)
amount;// 以当前节点为根的集合的元素总数(集合大小)
}f[N];
// 查询x的根节点,核心递归过程:仅更新参数,完全不需要遍历
// 不仅找根,更回溯更新路径上所有节点的父节点和路径长度
int fa(int x){
if(x!=f[x].f){// 若自身非根,触发递归更新逻辑
int oldf=f[x].f;// 暂存更新前的父节点(仅用于路径长度计算,无需遍历)
f[x].f=fa(f[x].f);// 递归查询父节点的根,回溯时更新当前节点的根为最终根
f[x].h+=f[oldf].h;//把长度列归到别的队列后,在fa就能更新路径长度
}
return f[x].f;// 返回最终根节点(递归终点+回溯起点,全程无遍历)
}
// 合并操作:将x所在集合(战舰列)连接到y所在集合(战舰列)
// 核心:仅修改根节点的三个参数,完全不需要遍历任何队列/集合元素
void unite(int x,int y){
int fx=fa(x),fy=fa(y);// 先查询并更新x、y的根(顺带完成路径压缩,无遍历)
if(fx==fy)return;// 同属一个集合,无需合并,无任何遍历操作
f[fx].f=fy;// 将x集合的根挂到y集合的根下,仅改参数,无需遍历合并队列
f[fx].h=f[fy].amount;// x集合根到新根的路径长度 = y集合总数,无遍历
f[fy].amount+=f[fx].amount;// 合并集合大小,仅参数累加,无任何遍历
}
int t,x,y;
char op;
int main(){
// 初始化:每个节点自成集合,仅设置三个核心参数,无遍历
for(int i=1;i<N;i++)f[i]={i,0,1};
cin>>t;
while(t--){
cin>>op>>x>>y;
if(op=='M')unite(x,y);// 合并操作:仅参数修改,无遍历
else{
int fx=fa(x),fy=fa(y);// 查询根的同时更新参数,无遍历
if(fx==fy){// 同属一个集合,计算两战舰间的数量
// 仅靠路径长度参数计算结果,完全不需要遍历战舰队列
cout<<abs(f[x].h-f[y].h)-1<<endl;
}
else cout<<"-1\n";// 不同集合,无关联,无遍历
}
}
return 0;
}
小结
并查集体现了递归的玄妙,这里的递归时刻都在发生,不仅是查询根更要更新根。
把a集合连接到b集合中,用数组还是容器,其实这里根本不需要遍历某个根元素。
核心递归逻辑:fa()函数的递归不仅是查询根节点,更在回溯时动态更新路径上所有节点的父节点和路径长度,这是并查集路径压缩的精髓;
极简参数设计:仅用f(根)、h(路径长)、amount(集合大小)三个参数维护集合关系,无需遍历操作,体现了并查集的精炼性;就能完成题目所有要求。
合并与查询逻辑:合并时仅修改根节点的关联参数,查询时通过路径长度差计算节点间距,完全契合战舰问题的核心需求。