1. 程式人生 > >樹鏈剖分原理和實現

樹鏈剖分原理和實現

原文連結

理解

樹鏈剖分就是將樹分割成多條鏈,然後利用資料結構(線段樹、樹狀陣列等)來維護這些鏈。

首先就是一些必須知道的概念:

  • 重結點:子樹結點數目最多的結點;
  • 輕節點:除了重節點以外的所有子節點;
  • 重邊:父親結點和重結點連成的邊;
  • 輕邊:父親節點和輕節點連成的邊;
  • 重鏈:由多條重邊連線而成的路徑;
  • 輕鏈:由多條輕邊連線而成的路徑;

在這裡插入圖片描述 比如上面這幅圖中,用黑線連線的結點都是重結點,其餘均是輕結點,2-11、1-11就是重鏈,其他就是輕鏈,用紅點標記的就是該結點所在鏈的起點,也就是我們?提到的top結點,還有每條邊的值其實是進行dfs時的執行序號。

演算法中定義了以下的陣列用來儲存上邊提到的概念: Alt

除此之外,還包括兩種性質: 如果(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);
}