把每一列看成一个并查集集合,维护每艘战舰到队头的距离;合并时整体挂到另一列后面,就能在线回答两舰之间隔了多少艘船。
OJ: luogu
题目 ID: P1196
难度:提高+/省选-
标签:并查集带权并查集合
日期: 2026-06-20 00:22
题意
一开始 1..30000 号战舰各自单独成列。
操作 M i j 表示:把 i 所在整列,按原顺序接到 j 所在整列的尾部。
操作 C i j 表示:如果 i 和 j 当前不在同一列,输出 -1;否则输出它们之间隔着多少艘战舰。
样例里的两次合并,可以只看相关的 4 艘战舰:
| 时刻 | 队列状态 |
|---|---|
| 初始 | [1] [2] [3] [4] |
M 2 3 后 |
[1] [3, 2] [4] |
M 2 4 后 |
[1] [4, 3, 2] |
所以最后询问 C 4 2 时,4 和 2 中间只有 3,答案是 1。
思路
先看一个直接模拟小数据的暴力:
cpp
// brute.cpp:直接按队列顺序模拟,只适合小数据理解和对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30005;
int q;
int belong[MAXN];
vector<int> ships[MAXN];
void init_queues() {
for (int i = 1; i <= 30000; i++) {
ships[i].clear();
ships[i].push_back(i);
belong[i] = i;
}
}
void merge_queue(int x, int y) {
int bx = belong[x];
int by = belong[y];
if (bx == by) {
return;
}
for (int ship : ships[bx]) {
ships[by].push_back(ship);
belong[ship] = by;
}
ships[bx].clear();
}
int query_between(int x, int y) {
int bx = belong[x];
int by = belong[y];
if (bx != by) {
return -1;
}
if (x == y) {
return 0;
}
int px = -1;
int py = -1;
for (int i = 0; i < (int)ships[bx].size(); i++) {
if (ships[bx][i] == x) {
px = i;
}
if (ships[bx][i] == y) {
py = i;
}
}
if (px > py) {
swap(px, py);
}
return py - px - 1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> q;
init_queues();
while (q--) {
char op;
int x, y;
cin >> op >> x >> y;
if (op == 'M') {
merge_queue(x, y);
} else {
cout << query_between(x, y) << '\n';
}
}
return 0;
}暴力把每一列真的存成一个数组:
- 合并时,把一整列逐个搬到另一列末尾
- 查询时,在这一列里找出两个编号的位置
这个做法很好理解,但大数据下太慢,因为一次合并就可能搬很多战舰。
这题的关键是:整列合并时,一列内部的相对顺序完全不变,只是整体往后平移了一段距离。
所以可以用带权并查集维护三件事:
fa[x]:x的父节点sz[x]:当x是根时,这一列一共有多少艘战舰dist_to_head[x]:x前面有多少艘战舰
这里把并查集的根看成这一列的队头。
如果把 x 所在整列接到 y 所在整列后面,设两个队头分别是 rx 和 ry,那么:
- 新队头仍然是
ry rx整列会整体后移sz[ry]个位置- 所以令
fa[rx] = ry,并设dist_to_head[rx] = sz[ry] - 再更新新队列长度
sz[ry] += sz[rx]
路径压缩时也要顺便把距离累加上去:
- 如果
x -> fa[x] -> root - 那么压缩后
dist_to_head[x] += dist_to_head[old_fa]
这样,查询同一列的两艘战舰时:
abs(dist_to_head[x] - dist_to_head[y])是它们位置差- 再减去两端自己,答案就是
abs(...) - 1
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30005;
int q;
int fa[MAXN], sz[MAXN], dist_to_head[MAXN];
void init_dsu() {
for (int i = 1; i <= 30000; i++) {
fa[i] = i;
sz[i] = 1;
dist_to_head[i] = 0;
}
}
int find_root(int x) {
if (fa[x] == x) {
return x;
}
int old_fa = fa[x];
fa[x] = find_root(fa[x]);
dist_to_head[x] += dist_to_head[old_fa];
return fa[x];
}
// 把 x 所在整列接到 y 所在整列的尾部。
void merge_queue(int x, int y) {
int rx = find_root(x);
int ry = find_root(y);
if (rx == ry) {
return;
}
// 并查集的根表示这一列的队头。
// x 这一列整体接到 y 这一列后面,所以 x 这一列的队头
// 前面会多出 sz[ry] 艘战舰。
fa[rx] = ry;
dist_to_head[rx] = sz[ry];
sz[ry] += sz[rx];
}
int query_between(int x, int y) {
int rx = find_root(x);
int ry = find_root(y);
if (rx != ry) {
return -1;
}
if (x == y) {
return 0;
}
return abs(dist_to_head[x] - dist_to_head[y]) - 1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> q;
init_dsu();
while (q--) {
char op;
int x, y;
cin >> op >> x >> y;
if (op == 'M') {
merge_queue(x, y);
} else {
cout << query_between(x, y) << '\n';
}
}
return 0;
}复杂度
设总操作数为 q。
并查集每次 find 和合并的均摊复杂度都近似常数,所以总时间复杂度是:
空间复杂度
总结
这题不是普通并查集,而是“并查集 + 相对位置维护”。
真正要抓住的是那个不变量:dist_to_head[x] 始终表示 x 前面有多少艘战舰。只要这个量在合并和路径压缩后都保持正确,查询答案就只是一个位置差。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

