【Coel.解題報告】【沒有憂慮的夢境世界】[HNOI2012]永無鄉
\(Never\) \(Land\) ,永遠的童年,不朽以及避世。
題前碎語
連寫了三篇筆記了,寫個解題報告調整一下心情。其實是因為 \(Splay\) 進階操作沒做完
題目簡介
P3224 [HNOI2012]永無鄉
洛谷傳送門
題目描述
永無鄉包含 \(n\) 座島,編號從 \(1\) 到 \(n\) ,每座島都有自己的獨一無二的重要度,按照重要度可以將這 \(n\) 座島排名,名次用 \(1\) 到 \(n\) 來表示。某些島之間由巨大的橋連線,通過橋可以從一個島到達另一個島。如果從島 \(a\) 出發經過若干座(含 \(0\) 座)橋可以 到達島 \(b\) ,則稱島 \(a\) 和島 \(b\)
現在有兩種操作:
B x y
表示在島 \(x\) 與島 \(y\) 之間修建一座新橋。
Q x k
表示詢問當前與島 \(x\) 連通的所有島中第 \(k\) 重要的是哪座島,即所有與島 \(x\) 連通的島中重要度排名第 \(k\) 小的島是哪座,請你輸出那個島的編號。
輸入輸出格式
輸入格式
第一行是用空格隔開的兩個整數,分別表示島的個數 \(n\) 以及一開始存在的橋數 \(m\)。
第二行有 \(n\) 個整數,第 \(i\) 個整數表示編號為 \(i\) 的島嶼的排名 \(p_i\)。
接下來 \(m\) 行,每行兩個整數 \(u, v\),表示一開始存在一座連線編號為 \(u\)
接下來一行有一個整數,表示操作個數 \(q\)。
接下來 \(q\) 行,每行描述一個操作。每行首先有一個字元 \(op\),表示操作型別,然後有兩個整數 \(x, y\)。
- 若 \(op\) 為
Q
,則表示詢問所有與島 \(x\) 連通的島中重要度排名第 \(y\) 小的島是哪座,請你輸出那個島的編號。 - 若 \(op\) 為
B
,則表示在島 \(x\) 與島 \(y\) 之間修建一座新橋。
輸出格式
對於每個詢問操作都要依次輸出一行一個整數,表示所詢問島嶼的編號。如果該島嶼不存在,則輸出 \(-1\) 。
資料規模與約定
- 對於 \(20\%\)
- 對於 \(100\%\) 的資料,保證 \(1 \leq m \leq n \leq 10^5\), \(1 \leq q \leq 3 \times 10^5\),\(p_i\) 為一個 \(1 \sim n\) 的排列,\(op \in \{\texttt Q, \texttt B\}\),\(1 \leq u, v, x, y \leq n\)。
解題思路
兩件事:查詢第 \(k\) 小,連線兩個集合。
查詢第 \(k\) 小可以用平衡樹解決(這裡使用 \(FHQ-Treap\) ),連線集合的話就要用並查集了。
注意在使用並查集連線集合的時候,需要進行啟發式合併。
啟發式合併聽起來很高大上,但實際上就是優先把小集合合併給大集合。
這樣做有什麼意義呢?直覺上告訴我們,大集合合併給小集合是比小集合合併給大集合慢的,所以採取啟發式合併可以降低時間複雜度,並且保持答案不變。
在路徑壓縮和啟發式合併的加成下,並查集能夠保持十分優秀的複雜度,平均情況下為 \(O(m\alpha(m,n))\) ,其中 \(\alpha(m,n)\) 是阿克曼函式的反函式,增長速度極其緩慢,可以認為是一個不超過 \(4\) 的常數;
而如果沒有使用啟發式合併,僅使用了路徑壓縮,極端情況下時間複雜度為 \(O(m\log n)\),效果大打折扣。
介紹了這麼多,迴歸正題吧。你還想起有正題
一些細節:
- 對於每個集合使用一棵平衡樹,所以不需要使用\(root\);
- 合併集合的時候跑一次 \(dfs\) 處理兩個集合的節點,注意引數的傳遞與否。
程式碼如下:
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <iostream>
namespace FastIO {
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch)) {
if (ch == '-')
f = -1;
ch = getchar();
}
while (isdigit(ch)) {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline void write(int x) {
if (x < 0) {
x = -x;
putchar('-');
}
static int buf[35];
int top = 0;
do {
buf[top++] = x % 10;
x /= 10;
} while (x);
while (top)
putchar(buf[--top] + '0');
puts("");
}
}
using std::swap;
using namespace FastIO;
const int maxn = 1e5 + 10;
int n, m, q;
int size[maxn], val[maxn], pri[maxn], ch[maxn][2], herb[maxn];
//herb用來存島嶼編號
int f[maxn];
int find(int x) {//路徑壓縮
return x == f[x] ? x : f[x] = find(f[x]);
}
void pushup(int x) {
size[x] = size[ch[x][0]] + size[ch[x][1]] + 1;
}
void split(int id, int k, int& x, int& y) {
if (id == 0)
x = y = 0;
else {
if (val[id] <= k) {
x = id;
split(ch[id][1], k, ch[id][1], y);
pushup(x);
} else {
y = id;
split(ch[id][0], k, x, ch[id][0]);
pushup(y);
}
}
}
int merge(int x, int y) {
if (x == 0 || y == 0)
return x + y;
if (pri[x] < pri[y]) {
ch[x][1] = merge(ch[x][1], y);
pushup(x);
return x;
} else {
ch[y][0] = merge(x, ch[y][0]);
pushup(y);
return y;
}
}
void insert(int& id, int x) {
int y, z, v = val[x];
split(id, v, y, z);
id = merge(merge(y, x), z);
}
void dfs(int x, int& y) {
if (x == 0)
return;
dfs(ch[x][0], y);
dfs(ch[x][1], y);
ch[x][0] = ch[x][1] = 0;
insert(y, x);
}
int Heuristic_Merge(int x, int y) {
if (size[x] > size[y])
swap(x, y);//啟發式合併的精髓
dfs(x, y);
return y;
}
void uni(int u, int v) {
int x = find(u), y = find(v), z;
if (x == y)
return;
z = Heuristic_Merge(f[u], f[v]);
f[x] = f[y] = f[z] = z;
}
int Query_kth(int id, int k) {
while (1) {
if (k <= size[ch[id][0]])
id = ch[id][0];
else if (k == size[ch[id][0]] + 1)
return id;
else {
k -= size[ch[id][0]] + 1;
id = ch[id][1];
}
}
}
int main() {
srand(3224);
n = read(), m = read();
for (int i = 1; i <= n; i++) {
f[i] = i;
val[i] = read();
pri[i] = rand();
size[i] = 1;
herb[val[i]] = i;
}
for (int i = 1; i <= m; i++) {
int u = read(), v = read();
uni(u, v);
}
q = read();
while (q--) {
char op[5];
scanf("%s", op + 1);//受cirno教導用字串輸入操作符
if (op[1] == 'B') {
int u = read(), v = read();
uni(u, v);
} else {
int x = read(), k = read();
if (size[find(x)] < k)
puts("-1");
else
write(herb[val[Query_kth(find(x), k)]]);
}
}
return 0;
}
題後閒話
不知道說什麼,就放個表情叭(