樹鏈剖分原理和實現
阿新 • • 發佈:2018-12-13
理解
樹鏈剖分就是將樹分割成多條鏈,然後利用資料結構(線段樹、樹狀陣列等)來維護這些鏈。
首先就是一些必須知道的概念:
- 重結點:子樹結點數目最多的結點;
- 輕節點:除了重節點以外的所有子節點;
- 重邊:父親結點和重結點連成的邊;
- 輕邊:父親節點和輕節點連成的邊;
- 重鏈:由多條重邊連線而成的路徑;
- 輕鏈:由多條輕邊連線而成的路徑;
比如上面這幅圖中,用黑線連線的結點都是重結點,其餘均是輕結點,2-11、1-11就是重鏈,其他就是輕鏈,用紅點標記的就是該結點所在鏈的起點,也就是我們?提到的top結點,還有每條邊的值其實是進行dfs時的執行序號。
演算法中定義了以下的陣列用來儲存上邊提到的概念:
除此之外,還包括兩種性質: 如果(u, v)是一條輕邊,那麼size(v) < size(u)/2; 從根結點到任意結點的路所經過的輕重鏈的個數必定都小與O(logn);
const int MAXN = (100000 << 2) + 10;
int siz[MAXN];//number of son
int top[MAXN];//top of the heavy link
int son[MAXN];//heavy son of the node
int dep[MAXN];//depth of the node
int faz[MAXN];//father of the node
int tid[MAXN ];//ID -> DFSID
int rnk[MAXN];//DFSID -> ID
演算法大致需要進行兩次的DFS,第一次DFS可以得到當前節點的父親結點(faz陣列)、當前結點的深度值(dep陣列)、當前結點的子結點數量(size陣列)、當前結點的重結點(son陣列)
void dfs1(int u, int father, int depth) {
/*
* u: 當前結點
* father: 父親結點
* depth: 深度
*/
// 更新dep、faz、siz陣列
dep[u] = depth;
faz[u] = father;
siz[u] = 1;
// 遍歷所有和當前結點連線的結點
for (int i = head[u]; i; i = edg[i].next) {
int v = edg[i].to; // 如果連線的結點是當前結點的父親結點,則不處理
if (v != faz[u]) {
dfs1(v, u, depth + 1); // 收斂的時候將當前結點的siz加上子結點的size
siz[u] += siz[v];
// 如果沒有設定過重結點son或者子結點v的siz大於之前記錄的重結點son,則進行更新
if (son[u] == -1 || siz[v] > siz[son[u]]) {
son[u] = v;
}
}
}
}
第二次DFS的時候則可以將各個重結點連線成重鏈,輕節點連線成輕鏈,並且將重鏈(其實就是一段區間)用資料結構(一般是樹狀陣列或線段樹)來進行維護,並且為每個節點進行編號,其實就是DFS在執行時的順序(tid陣列),以及當前節點所在鏈的起點(top陣列),還有當前節點在樹中的位置(rank陣列)。
void dfs2(int u, int t) {
/*
* u:當前結點
* t:起始的重結點
*/
top[u] = t; // 設定當前結點的起點為t
tid[u] = cnt; // 設定當前結點的dfs執行序號
rnk[cnt] = u; // 設定dfs序號對應成當前結點
cnt++;
// 如果當前結點沒有處在重鏈上,則不處理
if (son[u] == -1) {
return;
}
// 將這條重鏈上的所有的結點都設定成起始的重結點
dfs2(son[u], t);
// 遍歷所有和當前結點連線的結點
for (int i = head[u]; i; i = edg[i].next) {
int v = edg[i].to;
// 如果連線結點不是當前結點的重子結點並且也不是u的父親結點,將其的top設定成自己,進一步遞迴
if (v != son[u] && v != faz[u]){
dfs2(v, v);
}
}
}
而修改和查詢操作原理是類似的,以查詢操作為例,其實就是個LCA,不過這裡使用了top來進行加速,因為top可以直接跳轉到該重鏈的起始結點,輕鏈沒有起始結點之說,他們的top就是自己。需要注意的是,每次迴圈只能跳一次,並且讓結點深的那個來跳到top的位置,避免兩個一起跳從而插肩而過。
INT64 query_path(int x, int y) {
/**
* x:結點x
* y:結點y
* 查詢結點x到結點y的路徑和
*/
INT64 ans = 0;
int fx = top[x], fy = top[y];
// 直到x和y兩個結點所在鏈的起始結點相等才表明找到了LCA
while (fx != fy) {
if (dep[fx] >= dep[fy]) {
// 已經計算了從x到其鏈中起始結點的路徑和
ans += query(1, tid[fx], tid[x]);
// 將x設定成起始結點的父親結點,走輕邊,繼續迴圈
x = faz[fx];
} else {
ans += query(1, tid[fy], tid[y]);
y = faz[fy];
}
fx = top[x], fy = top[y];
}
// 即便找到了LCA,但是前面也只是分別計算了從一開始到最終停止的位置和路徑和
// 如果兩個結點不一樣,表明仍然需要計算兩個結點到LCA的路徑和
if (x != y) {
if (tid[x] < tid[y]) {
ans += query(1, tid[x], tid[y]);
} else {
ans += query(1, tid[y], tid[x]);
}
} else ans += query(1, tid[x], tid[y]);
return ans;
}
void update_path(int x, int y, int z) {
/**
* x:結點x
* y:結點y
* z:需要加上的值
* 更新結點x到結點y的值
*/
int fx = top[x], fy = top[y];
while(fx != fy) {
if (dep[fx] > dep[fy]) {
update(1, tid[fx],tid[x], z);
x = faz[fx];
} else {
update(1, tid[fy], tid[y], z);
y = faz[fy];
}
fx = top[x], fy = top[y];
}
if (x != y)
if (tid[x] < tid[y]) update(1, tid[x], tid[y], z);
else update(1, tid[y], tid[x], z);
else update(1, tid[x], tid[y], z);
}