【學習筆記】Kruskal 重構樹(BZOJ3551【ONTAK2010】Peaks加強版)
阿新 • • 發佈:2018-12-15
1. 例題引入:BZOJ3551
- 用一道例題引入:BZOJ3551
題目大意:有 座山峰,每座山峰有他的高度 。有些山峰之間有雙向道路相連,共 條路徑,每條路徑有一個困難值,這個值越大表示越難走,現在有 組詢問,每組詢問詢問從點 開始只經過困難值小於等於 的路徑所能到達的山峰中第 高的山峰的高度,如果無解輸出 。強制線上。
- 這道題的離線做法可以是線段樹合併,可以參照我之前寫過的一篇文章,裡面有提到:【學習筆記】線段樹的擴充套件(線段樹的合併與分裂、可持久化線段樹)
- 強制線上的話,我們似乎沒有什麼好思路。
- 先不考慮求第 大的權值,我們先考慮快速判斷點對 能否通過邊權不超過 的邊互相到達。
- 從最優化的角度考慮,把問題轉化為從點 出發,尋找一條 之間的路徑,使得這條路徑的邊權最大值最小。我們需要判斷的是這個最小的最大邊權是否不超過 。
- 因為無向圖形態固定,所以我們要找到一條 之間的路徑,使得這條路徑的邊權最大值最小,實際上就是找到最小生成樹中的 之間的路徑的最大邊權。
- 然而如果只是查詢最大邊權寫個樹上倍增就沒了。
- 這裡我們需要訪問從點 出發,能到達所有點的集合。很明顯,這個集合是一個連通塊,在 演算法的過程中,這個連通塊必然在某一時刻是完整存在於一個集合的(因為邊一定從小到大接進來的),我們利用這一點,可以將點按照它們之間能到達的最大邊權進行分類,於是就有了 重構樹。
2. Kruskal 重構樹的構造過程
- 具體做法:
- 我們把新構建出的圖叫做重構樹,開始重構樹中只有 個孤立的點,我們將它們的點權視作 。
- 在 演算法求最小生成樹的過程中,遇到一條連線兩個不同集合的邊,我們在並查集中分別找到兩個集合的根 ,新建一個結點 ,合併兩個集合,並且令 為新集合的根。
- 在重構樹中將 作為 共同的父親,即在重構樹中連邊 。令 的點權為 的邊權。
3. Kruskal 重構樹的性質
- 根據此構造過程,我們可以得到關於重構樹的性質:
- 重構樹是一棵二叉樹,且滿足大根堆的性質。
- 原圖中的 個點均為重構樹中的葉子結點。
- 對於點對 ,它們在原圖中的所有路徑中,最大邊權最小的路徑的最大邊權為, 在重構樹中 的權值。
- 對於一個葉子結點 ,它在原圖中經過邊權不超過 的邊,能到達的點集為:找到一個深度最小的 的祖先 ,使得 的點權不超過 ,根據 演算法的過程和重構樹的性質,可以知道, 的子樹中的葉子結點集合即為能到達的點集。對於一個葉子結點 ,它在原圖中經過邊權不超過 的邊,能到達的點集為:找到一個深度最小的 的祖先 ,使得 的點權不超過 ,根據 演算法的過程和重構樹的性質,可以知道, 的子樹中的葉子結點集合即為能到達的點集。
4. 回到例題:BZOJ3551
- 那麼題目中的詢問,我們利用性質4,在重構樹中找到深度最小的滿足條件的結點 。
- 然後求子樹的葉子節點中的 大權值,我們可以轉化為 序的區間內的 大權值,然後就是經典的靜態區間 大問題。
- 這個只需要對 序的每個字首 利用主席樹(可持久化線段樹)維護出權值線段樹的每個值域區間的元素個數,查詢時候只需要差分一下,在兩棵權值線段樹上二分即可。
- 如果不清楚靜態區間 大的可以自行百度搜索一下主席樹。
- 總結一下,對於圖的形態不變,並且需要限制通過邊權小於或大於某個值的邊,關於點的連通性的線上查詢問題,可以考慮 重構樹。
5. 相關題目
附:例題 BZOJ3551 程式碼
#include <bits/stdc++.h>
inline char nextChar()
{
static const int buffer_size = 2333333;
static char buffer[buffer_size];
static const char *tail = buffer + buffer_size;
static char *head = buffer + buffer_size;
if (head == tail)
{
fread(buffer, 1, buffer_size, stdin);
head = buffer;
}
return *head++;
}
template <class T>
inline void read(T &x)
{
static char ch;
while (!isdigit(ch = nextChar()));
x = ch - '0';
while (isdigit(ch = nextChar()))
x = x * 10 + ch - '0';
}
inline void putChar(char ch)
{
static const int buffer_size = 2333333;
static char buffer[buffer_size];
static const char *tail = buffer + buffer_size;
static char *head = buffer;
if (ch == '\0')
fwrite(buffer, 1, head - buffer, stdout);
*head++ = ch;
if (head == tail)
fwrite(buffer, 1, buffer_size, stdout), head = buffer;
}
template <class T>
inline void putint(T x)
{
static char buf[22];
static char *tail = buf;
if (!x) return (void)(putChar('0'));
if (x < 0) x = ~x + 1, putChar('-');
for (; x; x /= 10) *++tail = x % 10 + '0';
for (; tail != buf; --tail) putChar(*tail);
}
const int MaxNV = 2e5 + 5;
const int MaxNE = 5e5 + 5;
const int MaxLog = 20;
const int MaxS = MaxNV * 30;
struct edge
{
int u, v, w;
inline bool operator < (const edge &rhs) const
{
return w < rhs.w;
}
inline void scan()
{
read(u), read(v), read(w);
}
}e[MaxNE];
struct halfEdge
{
int v;
halfEdge *next;
}adj_pool[MaxNE], *adj[MaxNV], *adj_tail = adj_pool;
int n, m, Q, dfs_clock, last_ans, tot;
int rt, cnt, idx[MaxNV], seg[MaxNV];
int h[MaxNV], lef[MaxNV], rit[MaxNV];
int dep[MaxNV], anc[MaxNV][MaxLog + 1];
int ufs_fa[MaxNV], val[MaxNV];
int real_num, real[MaxNV];
int lc[MaxS], rc[MaxS], sze[MaxS];
inline void addEdge(int u, int v)
{
adj_tail->v = v;
adj_tail->next = adj[u];
adj[u] = adj_tail++;
}
inline int ufs_find(int x)
{
return x == ufs_fa[x] ? x : ufs_fa[x] = ufs_find(ufs_fa[x]);
}
inline int jump(int u, int k)
{
for (int i = MaxLog; i >= 0; --i)
if (anc[u][i] && val[anc[u][i]] <= k)
u = anc[u