1. 程式人生 > >樹鏈剖分之HLD

樹鏈剖分之HLD

樹鏈剖分的原理

對於陣列這樣的線性結構,要在其上實現區間查詢(和與最值)、區間修改甚至是區間增刪都是有辦法的。例如Sparse Table演算法、樹狀陣列、線段樹以及伸展樹等。而樹是一種非線性結構,為了高效的實現在樹上的路徑查詢、路徑修改操作,基本做法是將樹按照某種方式剖成若干條鏈,再將這些鏈按照順序組成陣列,最後在陣列上採用線段樹等手段實現操作。
考慮如下一棵樹:
原樹
可以把它剖成6條鏈,分別是ACHLNGIMBFKEDJ,因此對應的新陣列是
這裡寫圖片描述

輕重邊剖分

輕重邊剖分(Heavy-light Decomposition)是樹鏈剖分常用的一種方法,上面的例子就是一個HLD的例子。HLD可以用來解決一大類問題:在樹的結構形態不發生變化的前提下,實現路徑操作和子樹操作(修改、查詢和及最值)


重兒子:某個節點的子節點,其子樹節點數量最大,則該子節點為該節點的重兒子。其餘為輕兒子。在上例中,CA的重兒子,HC的重兒子,FB的重兒子。
重邊:父節點和重兒子之間的邊稱為重邊。在上例中,ACCHBF都是重邊。對應的,父節點和輕兒子的邊就稱為輕邊
重鏈:全部由重邊構成的路徑稱為重鏈,又稱為重路徑。在上例中,ACHLNGIMBFKEDJ就是重鏈。
N個節點的樹的任意一條路徑上,最多隻有logN條重鏈。每一條重鏈就是對應陣列的一個連續的部分,因此其上的操作最多為logN。因此每次樹上操作的時間為log2N
考慮每一次路徑操作,我們需要知道路徑到底是由哪些重鏈組成的以及這些重鏈在陣列中對應的部分。路徑上的重鏈資訊顯然是可以求出的,但是不必專門求出,在操作中不停更新重鏈即可。為了更新重鏈資訊,對於每個節點還需記錄深度資訊d
epth
以及每個節點所在重鏈的最頂層節點top。深度資訊比較容易理解。top資訊實際上就是每條鏈的第一個節點,例如在上例中Htop就是ALtop也是A,而Ktop則是B
有了depthtop資訊,即可進行路徑操作。考慮KM之間的路徑操作。因為KMtop不同,所以可以肯定KM不是一條重鏈。又因為Mtop深度更深,所以可以肯定M及其top之間的重鏈全部都在路徑上,可以進行一次區間操作;該次操作完成以後就跳到節點C,因為節點CMtop的父節點。再次考慮KC,因為Ktop深度更深,所以BK全部在路徑上,進行一次區間操作;然後跳到AACtop相同,說明AC是最後一段區間,對其進行操作即可。
因為輕重邊的區別完全取決於子樹的節點數量,每個節點設定一個s
ize
域,在一次深搜中就能確定size的值,同時可以確定每個節點的重兒子,以及每個節點的depth資訊與父節點。確定重兒子以後,再一次深搜就可以確定重鏈的順序以及每個節點的top。此次深搜的順序即轉化後的陣列的索引序號,然後對該陣列建樹狀陣列或者線段樹並進行操作即可。因此,整個解法複雜度為O(N+Qlog2N)。其中Q為操作的總數量。

實現

hdu3966是一個典型的樹鏈剖分題目,樹的形態維持不變,路徑更新、單點查詢,剖分完以後使用帶延遲的線段樹即可。

//#pragma comment(linker, "/STACK:1024000000,1024000000") 
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

#define SIZE 50010

struct edge_t{
    int node;//另一個節點的編號
    int next;//下一條邊的地址,實際地址為Edge+next
}Edge[SIZE<<1];
int ECnt = 0;
int Vertex[SIZE];//鄰接表表示所有的邊

//a、b之間建立兩條無向邊
void mkEdge(int a,int b){
    Edge[ECnt].node = b;
    Edge[ECnt].next = Vertex[a];
    Vertex[a] = ECnt ++;

    Edge[ECnt].node = a;
    Edge[ECnt].next = Vertex[b];
    Vertex[b] = ECnt ++;
}

struct node_t{
    int parent;   //父節點
    int heavy_son;//重邊的子節點
    int depth;     //深度
    int size;     //size域
    int top;      //本節點所在的重鏈的最頂層節點
    int nid;      //在dfs重鏈時重新對節點編號,確保同一條鏈上的節點相鄰
    int weight;   //權值,本題中即所求的數量
}Node[SIZE];
int TIdx = 0;
int NewIdx[SIZE];//新編號到原樹節點的對映
int N,M,P;

//dfs找出所有重邊,t為節點,parent為其父節點,depth為深度
//該dfs實際上確立了樹的結構
void dfsHeavyEdge(int t,int parent,int depth){
    Node[t].depth = depth;
    Node[t].parent = parent;
    Node[t].size = 1;
    //對t的所有子節點
    for(int next=Vertex[t];next;next=Edge[next].next){
        int u = Edge[next].node;
        if ( u == parent ) continue;
        dfsHeavyEdge(u,t,depth+1);
        Node[t].size += Node[u].size;
        //判斷重邊
        if ( Node[u].size > Node[Node[t].heavy_son].size )
            Node[t].heavy_son = u;
    }
}

//dfs找出所有重鏈,t為節點,top為當前節點所在重鏈的最頂層節點
//重鏈實際上是以其頂層節點為標識儲存的,任意節點都能夠直接得出其所在重鏈的頂層節點
void dfsHeavyPath(int t,int top){
    Node[t].top = top;
    Node[t].nid = TIdx++;
    NewIdx[Node[t].nid] = t;

    //t沒有重兒子,實際上就是葉節點
    if ( 0 == Node[t].heavy_son ) return;
    dfsHeavyPath(Node[t].heavy_son,top);

    //對t的所有節點
    for(int next=Vertex[t];next;next=Edge[next].next){
        int u = Edge[next].node;
        if ( u == Node[t].parent 
            || u == Node[t].heavy_son ) continue;

        dfsHeavyPath(u,u);
    }
}

//帶延遲的求區間和的線段樹
struct stnode_t{
    int peak;
    int delay;
}ST[SIZE<<2];

inline int lson(int t){return t<<1;}
inline int rson(int t){return (t<<1)|1;}

void _pushUp(int t){ST[t].peak=ST[lson(t)].peak+ST[rson(t)].peak;}
void _pushDown(int t){
    if ( 0 == ST[t].delay ) return;

    ST[lson(t)].delay += ST[t].delay;
    ST[lson(t)].peak += ST[t].delay;
    ST[rson(t)].delay += ST[t].delay;
    ST[rson(t)].peak += ST[t].delay;
    ST[t].delay = 0;
}

//建樹,遞迴建立節點
void build(int t,int s,int e){
    ST[t].delay = 0;

    if ( s == e ){
        ST[t].peak = Node[NewIdx[s]].weight;
        return;
    }
    int mid = ( s + e ) >> 1;
    build(lson(t),s,mid);
    build(rson(t),mid+1,e);
    _pushUp(t);
}
//[a,b]區間增加delta
void modify(int t,int s,int e,int a,int b,int delta){
    if ( a <= s && e <= b ){
        ST[t].delay += delta;
        ST[t].peak += delta;
        return;
    }

    _pushDown(t);
    int mid = ( s + e ) >> 1;
    if ( a <= mid ) modify(lson(t),s,mid,a,b,delta);
    if ( mid < b ) modify(rson(t),mid+1,e,a,b,delta);
    _pushUp(t);
}
//單點查詢
int query(int t,int s,int e,int idx){
    if ( s == e ) return ST[t].peak;
    _pushDown(t);
    int mid = ( s + e ) >> 1;
    int ret = ( idx <= mid ) 
        ? query(lson(t),s,mid,idx)
        : query(rson(t),mid+1,e,idx);
    _pushUp(t);
    return ret;
}

//關鍵操作,將原樹上(x,y)路徑的所有節點權值增加val
void change(int x,int y,int val){
    //當x,y不處於同一條重鏈
    while( Node[x].top != Node[y].top ){
        //令x所處的重鏈總是更深
        if ( Node[Node[x].top].depth < Node[Node[y].top].depth )
            swap(x,y);
        //將x所在的鏈的鏈頂到x的區間進行修改
        modify(1,1,N,Node[Node[x].top].nid,Node[x].nid,val);
        //將x修改為原鏈頂的父親,實質上就是跳到了另外一條鏈
        x = Node[Node[x].top].parent;
    }
    //到此處時,x、y處於同一條鏈,令x總是更淺,此舉是為了確定左右區間
    if ( Node[x].depth > Node[y].depth ) swap(x,y);
    //將x、y之間的路徑更新
    modify(1,1,N,Node[x].nid,Node[y].nid,val);
}
inline void init(){
    ECnt = TIdx = 1;
    fill(Vertex,Vertex+N+1,0);
    for(int i=0;i<=N;++i)Node[i].heavy_son = 0;
}
bool read(){
    if ( EOF == scanf("%d%d%d",&N,&M,&P) )
        return false;

    init();
    for(int i=1;i<=N;++i)scanf("%d",&Node[i].weight);
    for(int i=0;i<M;++i){
        int a,b;
        scanf("%d%d",&a,&b);
        mkEdge(a,b);
    }
    //以第1個節點為根建樹
    dfsHeavyEdge(1,0,0);
    //從根節點開始遞迴建重鏈
    dfsHeavyPath(1,1);
    //以ST[1]為根節點區間[1,N]建線段樹
    build(1,1,N);
    return true;
}
char Cmd[5];
int main(){
    while( read() ){
        while(P--){
            scanf("%s",Cmd);
            if ( 'Q' == *Cmd ){
                int x;
                scanf("%d",&x);
                printf("%d\n",query(1,1,N,Node[x].nid));
            }else{
                int x,y,v;
                scanf("%d%d%d",&x,&y,&v);
                if ( 'D' == *Cmd ) v = -v;
                change(x,y,v);
            }
        }
    }
    return 0;
}

相關推薦

分之HLD

樹鏈剖分的原理 對於陣列這樣的線性結構,要在其上實現區間查詢(和與最值)、區間修改甚至是區間增刪都是有辦法的。例如Sparse Table演算法、樹狀陣列、線段樹以及伸展樹等。而樹是一種非線性結構,為了高效的實現在樹上的路徑查詢、路徑修改操作,基本做法是將樹按

HLD解決子問題

樹鏈剖分HLD可以將非線性的樹結構轉換為由若干條樹鏈構成的陣列,並對陣列採用線段樹等手段,在一個令人比較滿意的時間複雜度內完成樹上路徑操作,包括路徑查詢和路徑修改。實際上,樹鏈剖分也可以完成子樹相關的操作,而且只需稍許改進即可。 考慮這樣一個樹以及剖分

蒟蒻淺談分之一——兩個dfs操作

val 不能 top pan 淺談 color void 情況 跳過 樹鏈剖分,顧名思義就是將樹形的結構剖分成鏈,我們以此便於在鏈上操作 首先我們需要明白在樹鏈剖分中的一些概念 重兒子:某節點所有兒子中子樹最多的兒子 重鏈:有重兒子構成的鏈 dfs序:按重兒子優先遍

bzoj2588 -- 分+主席

stat dfs strong pri 權值線段樹 iostream top pan splay 先將權值離散。 顯然可以對於每個結點建一棵權值線段樹存這個點到根結點的路徑上的點權,詢問時在線段樹上二分,但這樣時間是O(n2log2n)的。 然後想到用主席樹優化,時間復雜度

分求LCA

bsp 兩個 pla str 空間 num isp gif 節點和 樹鏈剖分求LCA 樹鏈剖分需要將樹的邊分為重邊和輕邊。每個節點和他的兒子之間只能有一條重邊,連接著該節點與他兒子中子樹節點最大的一個。一系列連續起來的重邊叫做重鏈,重鏈上的每個點的top值都是重鏈的頂端節點

【bzoj2836】魔法 分+線段

urn fin pan online char font -s class efi 題目描述 輸入 輸出 樣例輸入 4 0 1 1 2 2 3 4 Add 1 3 1 Query 0 Query 1 Query 2 樣例輸出

分的一種用法

我們 祖先 單點 數組 樹狀數組 實現 修改 相加 比較 這篇文章好像發得有點遲了啊QAQ之前忘了發了 又好久沒更了,講一個提高組內容。 我們來考慮一個有趣的問題,我們有一棵有根樹,每個點有點權,要求支持單點加,子樹加。 詢問比較奇怪,每個點有一個點權x,假裝不變,每

【LSGDOJ1834 Tree】

done using 給定 continue 返回 ace 最大的 接下來 chan 題目描述 給定一個N個結點的無向樹,樹中的結點按照1...N編號,樹中的邊按照1...N ? 1編號,每條邊都賦予一個權值。你需要編寫程序支持以下三種操作: 1. CHANGE i

BZOJ 2157 旅行(分碼農題)

tac pragma code vector zoj pla none close tag 寫了5KB,1發AC。。。 題意:給出一顆樹,支持5種操作。 1.修改某條邊的權值。2.將u到v的經過的邊的權值取負。3.求u到v的經過的邊的權值總和。4.求u到v的經過的邊的權值最

分膜版

logs mes main left ccf init root return getc 十分weak的樹鏈剖分初步 給一棵樹,實現兩個功能: ①給兩個節點u,v,給u,v路上的每條邊權值加a ②給兩個節點u,v,求u,v路上所有邊的權值之和 ps:在線操作

[bzoj 2243]: [SDOI2011]染色 [分][線段]

節點 query ext tran pac led str 包含 sans Description 給定一棵有n個節點的無根樹和m個操作,操作有2類: 1、將節點a到節點b路徑上所有點都染成顏色c; 2、詢問節點a到節點b路徑上的顏色段數量(連續相同顏色被認為是同

分模板

amp pan mes bsp -m 依次 代碼 clu tree P3384 【模板】樹鏈剖分 題目描述 如題,已知一棵包含N個結點的樹(連通且無環),每個節點上包含一個數值,需要支持以下操作: 操作1: 格式: 1 x y z 表示將樹從x到y結點最短路徑上所有節點的

洛谷——P3384 【模板】

upload mes 事情 -- aliyun pro 格式 路徑 sca https://www.luogu.org/problem/show?pid=3384#sub 題目描述 如題,已知一棵包含N個結點的樹(連通且無環),每個節點上包含一個數值,需要支持以下操作:

分 BZOJ3589 動態

ext 個數字 ret mst sin 2個 tput spa mit 3589: 動態樹 Time Limit: 30 Sec Memory Limit: 1024 MBSubmit: 543 Solved: 193[Submit][Status][Discuss]

BZOJ 3531 SDOI2014 旅行

can algorithm 一個點 復雜度 顏色 正常 track log case 題目大意:給定一棵樹,每一個點有一個權值和一個顏色。多次改變一些點的權值和顏色,多次求一條路徑上與起點和終點顏色同樣的點的權值和以及權&#

BZOJ2243 [SDOI2011]染色(分+線段合並)

ech sca 註意 get printf truct bre sum lca 題目鏈接 BZOJ2243 樹鏈剖分+線段樹合並 線段樹合並的一些細節需要註意一下 #include <bits/stdc++.h> using namespace std;

[CodeVS4633][Mz]分練習

blog pub != string query log dfs tdi sig 思路: 輕重鏈剖分+線段樹。 1 #include<cstdio> 2 #include<vector> 3 #include<cstri

【BZOJ1969】[Ahoi2005]LANE 航線規劃 離線+分+線段

個數 wap 鎖定 樹邊 mes zoj swap 相同 swa 【BZOJ1969】[Ahoi2005]LANE 航線規劃 Description 對Samuel星球的探險已經取得了非常巨大的成就,於是科學家們將目光投向了Samuel星球所在的星系—&md

【BZOJ2843】極地旅行社 離線+分+狀數組

i++ data 代碼 == 範圍 cst string input 樹狀 【BZOJ2843】極地旅行社 Description 不久之前,Mirko建立了一個旅行社,名叫“極地之夢”。這家旅行社在北極附近購買了N座冰島,並且提供觀光服務。

codevs 4633 [Mz]分練習

push pan span 等級 case des ++ spa 10個 時間限制: 1 s 空間限制: 64000 KB 題目等級 : 大師 Master 題目描述 Description 給定一棵結點數為n的樹,初始點權均為0