[NOI2002] 银河英雄传说

GitHub跳转原题关系图返回列表

把每一列看成一个并查集集合,维护每艘战舰到队头的距离;合并时整体挂到另一列后面,就能在线回答两舰之间隔了多少艘船。

OJ: luogu

题目 ID: P1196

难度:提高+/省选-

标签:并查集带权并查集合

日期: 2026-06-20 00:22

题意

一开始 1..30000 号战舰各自单独成列。

操作 M i j 表示:把 i 所在整列,按原顺序接到 j 所在整列的尾部。

操作 C i j 表示:如果 ij 当前不在同一列,输出 -1;否则输出它们之间隔着多少艘战舰。

样例里的两次合并,可以只看相关的 4 艘战舰:

时刻 队列状态
初始 [1] [2] [3] [4]
M 2 3 [1] [3, 2] [4]
M 2 4 [1] [4, 3, 2]

所以最后询问 C 4 2 时,42 中间只有 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 所在整列后面,设两个队头分别是 rxry,那么:

  1. 新队头仍然是 ry
  2. rx 整列会整体后移 sz[ry] 个位置
  3. 所以令 fa[rx] = ry,并设 dist_to_head[rx] = sz[ry]
  4. 再更新新队列长度 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 和合并的均摊复杂度都近似常数,所以总时间复杂度是:

O(qα(30000))O(q \alpha(30000))

空间复杂度 O(30000)O(30000)

总结

这题不是普通并查集,而是“并查集 + 相对位置维护”。

真正要抓住的是那个不变量:dist_to_head[x] 始终表示 x 前面有多少艘战舰。只要这个量在合并和路径压缩后都保持正确,查询答案就只是一个位置差。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析