樹鏈剖分
重鏈剖分
【問題引入】
問題描述
給定一顆有 $n$ 個節點、帶邊權的樹,現在有對樹進行 $m$ 個操作,操作有 $2$ 類:
- 將節點 $a$ 到節點 $b$ 路徑上所有邊權的值都改為 $c$;
- 詢問節點 $a$ 到節點 $b$ 路徑上的最大邊權值。
請你寫一個程式依次完成這 $m$ 個操作。
有三個操作
- 修改 $2$ 號到 $9$ 號節點路徑上的邊值為 $7$;
- 修改 $2$ 號到 $7$ 號節點路徑上的邊值為 $4$ ;
- 查詢 $9$ 號到 $8$ 號節點路徑上的最大邊權值。
每次修改和查詢複雜度為 $O(n)$。
$\color{red}思考:既然每個操作都與樹上的路徑有關,能否把這些路徑分段儲存,方便修改和查詢?$
【樹鏈剖分的概念】
樹鏈剖分是指一種對樹進行劃分的演算法,它先透過輕重邊剖分 $(Heavy-Light\ Decomposition)$ 將樹分為多條鏈,保證每個點屬於且只屬於其中一條鏈,然後再透過資料結構(樹狀陣列、SBT
、SPLAY
、線段樹等)來維護每一條鏈。
使用這種方法後,一般可以將修改和查詢的複雜度降為 $O(log_2\ n)$。
【樹鏈剖分的方法】
定義
將樹中的邊分為:重邊和輕邊。
定義 $size(X)$ 為以 $X$ 為根的子樹的節點個數。
令 $V$ 為 $U$ 的兒子節點中 $size$ 值最大的節點,那麼 $V$ 稱為 $U$ 的$\color{red}重兒子$,邊 $(U,V)$ 被稱為$\color{red}重邊$,樹中重邊之外的邊被稱為$\color{red}輕邊$,全部由重邊構成的路徑稱為$\color{red}重鏈$。
性質1
對於輕邊 $(U,V)$,$size(V)\le size(U)\div 2$。
從根到某一點的路徑上,不超過 $log_2 N$ 條輕邊,不超過 $log_2 N$ 條重路徑。
特例
圖中有 $5$ 條重鏈:
- $1\to 3\to 6\to 10$
- $2\to 5\to 9$
- $4$
- $7$
- $8$
性質2
每一條鏈首深度大於 $1$ 的重鏈都可以透過其$\color{red}鏈首的父親節點$連到$\color{red}另一條重鏈$上。
核心定義
$size[i]$:以節點 $i$ 為根的子樹中節點的個數;
$son[i]$:節點 $i$ 的重兒子;
$dep[i]$:節點 $i$ 的深度,根的深度為 $1$;
$top[i]$:節點 $i$ 所在重鏈的鏈首節點;
$fa[i]$:節點 $i$ 的父節點;
$tid[i]$:在 DFS
找重鏈的過程中為節點 $i$ 重新編的號碼,每條重鏈上的編號節點是連續的。
兩次DFS
- 第一次
DFS
:找重邊,順便求出所有的 $size[i],dep[i],fa[i],son[i]$; - 第二次
DFS
:將重邊連成重鏈,順便求出所有的 $top[i],tid[i]$。
第一次DFS
找重邊,順便求出所有的 $size[i],dep[i],fa[i],son[i]$;
第二次DFS
將重邊連成重鏈,順便求出所有的 $top[i],tid[i]$。
從根節點開始 ,沿重邊向下擴充套件,連成重鏈;
不能加入當前重鏈上的節點,以該節點為鏈首向下拉一條新的重鏈(如果該節點是葉子節點,則自己構成一條重鏈);
DFS
過程中,對節點重新編號,因為是沿重邊向下擴充套件,故一條重鏈上的節點新編號會是連續的。
參考程式碼
void DFS_2(int u, int sp) {//第二遍dfs求出top[],p[],fp[]的值
top[u] = sp;
p[u] = pos++;
fp[p[u]] = u;
if (son[u] != -1) DFS_2(son[u], sp);//先處理重邊
for (int i = head[u]; i != -1; i = edge[i].next) {//再處理輕邊情況
int v = edge[i].to;
if (v != son[u] && v != prt[u])
DFS_2(v, v);//輕鏈的頂點就是自己
}
}
【樹鏈剖分的過程】
重鏈剖分後
剖分完成後,每條重鏈相當於一段區間,將所有的重鏈收尾相接,用適合的資料結構來維護這個整體。
- $tid[i]$:$\color{red}節點\ i$ 所對應的新編號
- $rank[i]$:$\color{red}編號\ i$ 所在原樹中對應的節點編號
我們可以採用 $\Large\color{red}線段樹$等資料結構維護每條重鏈。
以線段樹維護為例
維護節點 $u,v$ 路徑上的最大值
【樹鏈剖分的修改操作】
即整體修改點 $U$ 和點 $V$ 的路徑上每條邊的權值。
點 $U$ 和點 $V$ 的關係分為兩種情況:
- 情況 $1$ :$U$ 和 $V$ 在同一條重鏈上;
- 情況 $2$ :$U$ 和 $V$ 不在同一條重鏈上。
情況 $1$
以修改邊為例:將原樹中的邊($6\to 10$)權值修改為 $6$($\color{red}邊的權值存在邊所到達頂點中$)。
$10$ 號節點的新編號為 $4$,故只需修改線段樹中的 $[4,4]$ 的值即可。
$2$ 號和 $9$ 號節點的新編號分別為 $7$ 和 $9$,需要修改的區間為 $tid[son[2]]\sim tid[9]$,即 $[8,9]$。
情況 $2$
將原樹中路徑 ($8\to 9$)上所有邊的權值都修改為 $8,\ top[9]=2,\ top[8]=8$。
它們不在同一條重鏈上,需要分段修改,邊修改邊$\color{red}往一條重鏈上靠$。
優先將$\color{red}鏈首$深度大的點往上爬,向另一條重鏈靠,直到兩者爬到同一條重鏈,轉換為情況 $1$ 解決。
後面的圖不畫了。
【樹鏈剖分的查詢操作】
和修改操作類似。
設查詢 $u\to v\ \max$。
情況 $1$
$top[u] = top[v]$
線上段樹上查詢 $u\sim v$ 的區間即可。
情況 $2$
$top[u]\ne top[v]$
向上爬樹,深度大的優先,深度一樣隨便爬一個。
重複這個操作,直到 $top[u] = top[v]$,轉換成情況 $1$ 處理。
參考程式碼
int Change(int u, int v) {//查詢 u -> v 邊的最大值
int f1 = top[u], f2 = top[v];
int tmp = 0;
while (f1 != f2) {
//u 和 v 不在同一條重路徑,深度深的點向上爬,直到在同一條重(輕)路徑上為止
if (dep[f1] < dep[f2]) {
swap(f1, f2);
swap(u, v);
}
tmp = max(tmp, Query(1, p[f1], p[f2]));
//在重鏈中求 u 和鏈首端點 f1 路徑上的最大值
u = prt[f1];
f1 = top[u];
}
if (u == v) return tmp;//為同一點,則退出
if (dep[u] > dep[v])
swap(u, v);
return max(tmp, Query(1, p[son[u]], p[v]));
}
//Query(v, l, r) 查詢線段樹中 [l,r] 的最大值
實鏈剖分
沒有
長鏈剖分
沒有