题目地址:
https://www.acwing.com/problem/content/description/1065/
永无乡包含 n n n座岛,编号从 1 1 1到 n n n,每座岛都有自己的独一无二的重要度,按照重要度可以将这 n n n座岛排名,名次用 1 1 1到 n n n来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 a a a出发经过若干座(含 0 0 0座)桥可以到达岛 b b b,则称岛 a a a和岛 b b b是连通的。现在有两种操作:
B x y
表示在岛 x x x与岛 y y y之间修建一座新桥。
Q x k
表示询问当前与岛 x x x连通的所有岛中第 k k k重要的是哪座岛,即所有与岛 x x x连通的岛中重要度排名第 k k k小的岛是哪座,请你输出那个岛的编号。
输入格式:
第一行是用空格隔开的两个正整数 n n n和 m m m,分别表示岛的个数以及一开始存在的桥数。接下来的一行是用空格隔开的 n n n个数,依次描述从岛 1 1 1到岛 n n n的重要度排名。随后的 m m m行每行是用空格隔开的两个正整数 a i a_i ai和 b i b_i bi,表示一开始就存在一座连接岛 a i a_i ai和岛 b i b_i bi的桥。后面剩下的部分描述操作,该部分的第一行是一个正整数 q q q,表示一共有 q q q个操作,接下来的 q q q行依次描述每个操作,操作的格式如上所述,以大写字母Q
或B
开始,后面跟两个不超过 n n n的正整数,字母与数字以及两个数字之间用空格隔开。
输出格式:
对于每个Q x k
操作都要依次输出一行,其中包含一个整数,表示所询问岛屿的编号。如果该岛屿不存在,则输出 − 1 −1 −1。
数据范围:
对于 20 20 20的数据 n ≤ 1000 , q ≤ 1000 n≤1000,q≤1000 n≤1000,q≤1000,
对于 100 100 100的数据 n ≤ 100000 , m ≤ n , q ≤ 300000 n≤100000,m≤n,q≤300000 n≤100000,m≤n,q≤300000
由于要寻找与某个岛 x x x的所有连通的岛里排名第 k k k的岛的编号,所以想到要用平衡树,一开始有 n n n个平衡树,然后每次合并两个树,构成更大的连通块(这可以用启发式合并来做,每次合并的时候将节点个数少的树后序遍历一遍,将每个节点插入到节点个数多的树里。注意,这里必须得后序遍历,这样插入的时候就可以直接拿原节点本身插入,而不需要new新节点了,可以节省很多空间。由于启发式合并里,每个节点参与合并的次数是 O ( log n ) O(\log n) O(logn),因为每个节点所在的树的节点个数会一次次翻倍,合并一次它需要 O ( log n ) O(\log n) O(logn),一共 n n n个节点,所以合并上花的总时间复杂度为 O ( n log 2 n ) O(n\log^2n) O(nlog2n));并且,为了查询排名,可以将每个树节点里存一下它为根的子树的节点个数。这两者都可以用FHQ Treap来做。但是,光有平衡树还不够,给定岛屿编号,我们要能知道这个岛屿现在在哪棵树里,进而得到树根,然后才能进行查询和合并的操作,这就需要并查集。代码如下:
#include <iostream>
using namespace std;
// 开一倍空间即可
const int N = 1e5 + 10;
int n, m;
struct Node {
int l, r;
int val, id;
int sz, rnd;
} tr[N];
int idx;
// p是并查集数组,root[i]表示i号岛屿所在的FHQ Treap的
// 树根节点下标,只有在i是并查集树根的时候才有效
int root[N], p[N];
int get_node(int val, int id) {
tr[++idx].sz = 1;
tr[idx].val = val;
tr[idx].id = id;
tr[idx].rnd = rand();
return idx;
}
void pushup(int u) {
tr[u].sz = tr[tr[u].l].sz + tr[tr[u].r].sz + 1;
}
int merge(int x, int y) {
if (!x || !y) return x | y;
if (tr[x].rnd > tr[y].rnd) {
tr[x].r = merge(tr[x].r, y);
pushup(x);
return x;
} else {
tr[y].l = merge(x, tr[y].l);
pushup(y);
return y;
}
}
void split(int u, int val, int &x, int &y) {
if (!u) x = y = 0;
else {
if (tr[u].val <= val) {
x = u;
split(tr[u].r, val, tr[u].r, y);
} else {
y = u;
split(tr[u].l, val, x, tr[u].l);
}
pushup(u);
}
}
// 在编号为id的岛屿所在的FHQ Treap里查询排名第rank的节点的id
int get_id(int id, int rank) {
int u = root[id];
while (u) {
if (rank <= tr[tr[u].l].sz) u = tr[u].l;
else if (rank > tr[tr[u].l].sz + 1) {
rank -= tr[tr[u].l].sz + 1;
u = tr[u].r;
} else return tr[u].id;
}
return -1;
}
// 把下标为u的节点插入到b号岛屿所在的FHQ Treap里
void insert(int u, int b) {
int x, y;
// 按u的val将b所在的FHQ Treap分裂
split(root[b], tr[u].val, x, y);
// 再合并回来
root[b] = merge(merge(x, u), y);
}
// 后序遍历u,将每个节点插入编号为b的岛屿所在的树里
void dfs(int u, int b) {
if (!u) return;
dfs(tr[u].l, b);
dfs(tr[u].r, b);
insert(u, b);
}
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
p[i] = root[i] = get_node(x, i);
}
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
x = find(x), y = find(y);
if (x != y) {
if (tr[root[x]].sz > tr[root[y]].sz) swap(x, y);
p[x] = y;
dfs(root[x], y);
}
}
scanf("%d", &m);
while (m--) {
int x, y;
char op[2];
scanf("%s%d%d", op, &x, &y);
x = find(x);
if (*op == 'B') {
y = find(y);
if (x != y) {
if (tr[root[x]].sz > tr[root[y]].sz) swap(x, y);
p[x] = y;
dfs(root[x], y);
}
} else {
int k = y;
if (tr[root[x]].sz < k) puts("-1");
else printf("%d\n", get_id(x, k));
}
}
return 0;
}
时间复杂度 O ( n log 2 n + m log n ) O(n\log^2n+m\log n) O(nlog2n+mlogn),空间 O ( n ) O(n) O(n)。
也可以用Splay来做。代码如下:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
struct Node {
int s[2], p, v, id;
int sz;
void init(int _v, int _id, int _p) {
v = _v, id = _id, p = _p;
sz = 1;
}
} tr[N];
int root[N], idx;
int p[N];
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void pushup(int x) {
tr[x].sz = tr[tr[x].s[0]].sz + tr[tr[x].s[1]].sz + 1;
}
void rotate(int x) {
int y = tr[x].p, z = tr[y].p;
int k = tr[y].s[1] == x;
tr[z].s[tr[z].s[1] == y] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
tr[x].s[k ^ 1] = y, tr[y].p = x;
pushup(y), pushup(x);
}
void splay(int x, int k, int b) {
while (tr[x].p != k) {
int y = tr[x].p, z = tr[y].p;
if (z != k)
if ((tr[y].s[1] == x) ^ (tr[z].s[1] == y)) rotate(x);
else rotate(y);
rotate(x);
}
if (!k) root[b] = x;
}
void insert(int u, int b) {
int c = root[b], v = tr[u].v, p = 0;
while (c) p = c, c = tr[c].s[v > tr[c].v];
if (p) tr[p].s[v > tr[p].v] = u;
tr[u].p = p;
// 插入完了要将插入的节点伸展到树根
splay(u, 0, b);
}
int get_k(int k, int b) {
int u = root[b];
while (u) {
if (k <= tr[tr[u].s[0]].sz) u = tr[u].s[0];
else if (k > tr[tr[u].s[0]].sz + 1) {
k -= tr[tr[u].s[0]].sz + 1;
u = tr[u].s[1];
} else return tr[u].id;
}
return -1;
}
void dfs(int u, int b) {
if (!u) return;
if (tr[u].s[0]) dfs(tr[u].s[0], b);
if (tr[u].s[1]) dfs(tr[u].s[1], b);
insert(u, b);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
p[i] = root[i] = i;
int v;
scanf("%d", &v);
tr[i].init(v, i, 0);
}
idx = n;
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
a = find(a), b = find(b);
if (a != b) {
if (tr[root[a]].sz > tr[root[b]].sz) swap(a, b);
p[a] = b;
dfs(root[a], b);
}
}
scanf("%d", &m);
while (m--) {
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
a = find(a);
if (*op == 'B') {
b = find(b);
if (a != b) {
if (tr[root[a]].sz > tr[root[b]].sz) swap(a, b);
p[a] = b;
dfs(root[a], b);
}
} else {
if (tr[root[a]].sz < b) puts("-1");
else printf("%d\n", get_k(b, a));
}
}
return 0;
}
时空复杂度一样。