1. 程式人生 > 其它 >【Coel.解題報告】【沒有憂慮的夢境世界】[HNOI2012]永無鄉

【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\)

的島嶼和編號為 \(v\) 的島嶼的橋。

接下來一行有一個整數,表示操作個數 \(q\)

接下來 \(q\) 行,每行描述一個操作。每行首先有一個字元 \(op\),表示操作型別,然後有兩個整數 \(x, y\)

  • \(op\)Q,則表示詢問所有與島 \(x\) 連通的島中重要度排名第 \(y\) 小的島是哪座,請你輸出那個島的編號。
  • \(op\)B,則表示在島 \(x\) 與島 \(y\) 之間修建一座新橋。

輸出格式

對於每個詢問操作都要依次輸出一行一個整數,表示所詢問島嶼的編號。如果該島嶼不存在,則輸出 \(-1\)

資料規模與約定

  • 對於 \(20\%\)
    的資料,保證 \(n \leq 10^3\), \(q \leq 10^3\)
  • 對於 \(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;
}

題後閒話

不知道說什麼,就放個表情叭(