樹鏈剖分

zla_2012發表於2024-10-25

樹鏈剖分

重鏈剖分

【問題引入】

問題描述

給定一顆有 $n$ 個節點、帶邊權的樹,現在有對樹進行 $m$ 個操作,操作有 $2$ 類:

  • 將節點 $a$ 到節點 $b$ 路徑上所有邊權的值都改為 $c$;
  • 詢問節點 $a$ 到節點 $b$ 路徑上的最大邊權值。

請你寫一個程式依次完成這 $m$ 個操作。

有三個操作

  • 修改 $2$ 號到 $9$ 號節點路徑上的邊值為 $7$;

  • 修改 $2$ 號到 $7$ 號節點路徑上的邊值為 $4$ ;

  • 查詢 $9$ 號到 $8$ 號節點路徑上的最大邊權值。

每次修改和查詢複雜度為 $O(n)$

$\color{red}思考:既然每個操作都與樹上的路徑有關,能否把這些路徑分段儲存,方便修改和查詢?$

【樹鏈剖分的概念】

樹鏈剖分是指一種對樹進行劃分的演算法,它先透過輕重邊剖分 $(Heavy-Light\ Decomposition)$ 將樹分為多條鏈,保證每個點屬於且只屬於其中一條鏈,然後再透過資料結構(樹狀陣列、SBTSPLAY、線段樹等)來維護每一條鏈。

使用這種方法後,一般可以將修改和查詢的複雜度降為 $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] 的最大值

實鏈剖分

沒有

長鏈剖分

沒有

相關文章