1. 程式人生 > 實用技巧 >樹論演算法複習筆記

樹論演算法複習筆記

樹論演算法複習筆記

省選前寫的,現在發出來
目錄

樹的直徑

定義:樹的直徑是樹上的最長路徑。
值得注意的是,直徑並不唯一

直徑的性質

我們這裡只討論無負權的情況

  1. 直徑兩端點一定是葉子節點。
  2. 距任意點最遠點一定是直徑的端點,據所有點最大值最小的點一定是直徑的中點。
  3. 兩棵樹相連,新直徑的兩端點一定是原四個端點中的兩個
  4. 兩棵樹相連,新直徑長度最小為\(\max(\max(直徑1,直徑2),半徑1+半徑2+新邊長度)\)
    .其中直徑長度為奇數時半徑上取整。
  5. 一棵樹上接一個葉子結點,直徑最多改變一個端點
  6. 若一棵樹存在多條直徑,那麼這些直徑一定交於一點,且交點是直徑的嚴格中點(中點可能在某條邊內)
  7. 從一個點出發的最長路徑的終點一定是直徑的端點

BFS求法

從一個點出發的最長路徑的終點一定是直徑的端點
因此我們可以先隨便找一個點,從它出發BFS找到距離最遠的點\(s\),再從\(s\)出發BFS找到距它最遠的點\(t\),\(s\)\(t\)的路徑就是直徑. 注意如果樹有負權,就不能用此方法。

node bfs(int s){
    memset(used,0,sizeof(used));
    queue<node>q;
    q.push(node(s,0));
    node now,nex;
    int tx,ty;
    int maxt=0,maxx=s;
    used[s]=1;
    while(!q.empty()){
        now=q.front();
        q.pop();
        int u=now.x;
        if(now.t>maxt){
            maxx=now.x;
            maxt=now.t;
        }
        for(int i=head[u];i!=0;i=E[i].next){
            if(!used[E[i].to]){
                used[E[i].to]=1;
                q.push(node(E[i].to,now.t+E[i].len));
            }
        }
    }
    return node(maxx,maxt);
}
int main(){
    node tmp1=bfs(1);
    node tmp2=bfs(tmp1.x);
    printf("%d\n",tmp2.t);//輸出直徑長度
}

樹形DP求法

\(f_x\)表示當前已經合併的子樹內到\(x\)的最長路徑
在合併每個子樹前更新答案\(d=\max(d,f_x+f_y+w(x,y))\),然後\(f_x=\max_{y \in son(x)}(f_x,f_y+w(x,y))\)

void dfs(int x,int fa){
    for(int y=head[x];y;y=E[i].next){
        int y=E[i].to;
        if(y!=fa){
            dfs(y,x);
            ans=max(ans,f[x]+f[y]+E[i].len);
            f[x]=max(f[x],f[y]+E[i].len);
        }
    }
}

這種演算法可以處理負邊權.

[APIO2016]巡邏
在一個地區中有 n 個村莊,編號為 1, 2, ..., n。有 n – 1 條道路連線著這些村 莊,每條道路剛好連線兩個村莊,從任何一個村莊,都可以通過這些道路到達其 他任一個村莊。每條道路的長度均為 1 個單位。 為保證該地區的安全,巡警車每天要到所有的道路上巡邏。警察局設在編號 為 1 的村莊裡,每天巡警車總是從警察局出發,最終又回到警察局

為了減少總的巡邏距離,該地區準備在這些村莊之間建立 K 條新的道路, 每條新道路可以連線任意兩個村莊。兩條新道路可以在同一個村莊會合或結束。 一條新道路甚至可以是一個環,其兩端連線到同一 個村莊。 由於資金有限,K 只能是 1 或 2。同時,為了不浪費資金,每天巡警車必須 經過新建的道路正好一次。計算出最佳 的新建道路的方案使得總的巡邏距離最小,並輸出這個最小的巡邏距離。
當不建立新的道路時,路線總長度顯然為\(2(n-1)\),因為每條邊被走了2次

\(K=1\),建立一條新的道路\((x,y)\),會形成一個環,原來\(x\)\(y\)的路徑上的邊會少走一次。那麼讓\(x\),\(y\)為直徑的兩個端點最優。答案是\(2(n-1)-d_1+1\).其中\(d_1\)為原樹的直徑。

\(K=2\),再建立一條新的道路\((x',y')\),又會形成一個環。假如兩個環不重疊,那麼\(x',y'\)之間的路徑上的邊會少走一次.但如果環相交,那麼相交的那些邊會被減去兩次,相當於沒被巡邏到。因此我們要把第一次的路徑\((x,y)\)上的邊權設為\(-1\),這樣減掉-1之後相當於加回來一次,就合法了。在新的樹上求直徑\(d_2\),答案為\(2(n-1)-d_1+1-d_2+1=2n-d_1-d_2\).

樹的重心

定義:在樹上刪掉這個點後使得剩下連通塊大小的最大值最小的點稱為樹的重心。

重心的性質

  1. 樹中所有點到某個點的距離和中,到重心的距離和是最小的.
  2. 把兩棵樹通過一條邊相連,新的樹的重心在原來兩棵樹重心的連線上。
  3. 一棵樹新增或者刪除一個節點,樹的重心最多隻移動一條邊的位置。
  4. 一棵樹最多有兩個重心,且這兩個重心一定相鄰。
  5. 刪掉樹的重心後,每個連通塊的大小一定不超過原樹大小的一半(這是保證點分治複雜度正確的基礎)

求法

用樹形DP,設\(f_x\)為去掉\(x\)後的連通塊大小最大值,則\(f_x=\max(n-sz_x,\max_{y \in \operatorname{son}(x)}(sz_y)\),其中\(sz_x\)\(x\)的子樹大小.這是因為去掉\(x\)後樹會被分為\(x\)的兒子的子樹和整棵樹去掉\(x\)子樹後的樹

void dfs(int x,int fa){
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa){
            dfs(y,x);
            sz[x]+=sz[y];
            f[x]=max(f[x],sz[y]);
        }
    }
    f[x]=max(f[x],n-sz[x]);
    if(f[x]<f[root]) root=x;
}

[AGC 018D]給出一個n個點的帶邊權的樹T,再給出一個n個點的完全圖,完全圖中每兩個點之間的距離為這兩個點在樹上的距離,求完全圖最長的哈密頓路(\(n \leq 10^5\))
貪心考慮,對於原樹中的一條邊\((u,v,w)\),子樹大小為\(sz\),不難發現它的貢獻上界是\(\min(sz_u,n-sz_u)\cdot w\).並且存在一種方案使得每條邊都取到上界。那就是把樹分成大小為\(\lfloor \frac{n}{2} \rfloor\)的兩半,然後在兩半之間交替走。

但是重心只能訪問一次。如果有1個重心,那就說明最後一步不能回到重心,找一條與重心相連的最小的邊不走,把它從答案中減去。如果有2個重心,那麼不走的一定是兩個重心之間的邊。

程式碼

LCA

定義:對於有根樹上兩點\(x,y\),它們公共的祖先節點中深度最深的節點稱為它們的最近公共祖先(LCA),記為\(\operatorname{LCA}(x,y)\)

LCA的求法

顯然暴力向上跳可以求,複雜度為\(O(樹高)\),在一些保證樹高為\(\log\)級別的樹上(如並查集)跑的很快,因此可以和一些資料結構巢狀。

LCA的高效求法有很多種,且都依賴於樹相關的其他一些知識點。

  1. 樹上倍增求LCA,線上,複雜度\(O(n\log n)-O(\log n)\)(分別指預處理和查詢的時間複雜度,下同)
  2. 樹鏈剖分求LCA,線上,複雜度\(O(n)-O(\log n)\)
  3. ST表+尤拉序求LCA,線上,複雜度\(O(n\log n)-O(1)\),可以用毒瘤的奇技淫巧優化到\(O(n)-O(1)\)
  4. LCA的Tarjan演算法,離線,複雜度\(O(n\alpha(n))-O(1)\),用處不大,這裡不贅述。

在學習LCA的性質之前先掌握1.之後要熟練掌握1和2.

樹上差分

樹上兩點距離公式:
\(d_x\)表示\(x\)到根距離,那麼\(\operatorname{dist}(x,y)=d_x+d_y-2d_{\operatorname{LCA}(x,y)}\)
這是因為\(\operatorname{LCA}(x,y)\)向上到根的路徑被算了兩次。

[BZOJ 3307]Cow Politics
給出一棵N個點的樹,樹上每個節點都有顏色。對於每種顏色,求該顏色距離最遠的兩個點之間的距離。N≤200000
顯然對於每種顏色建立一棵虛樹是可行的。但是有編碼複雜度更低的方法。顯然某種顏色距離最遠的兩個點中,一個肯定是這種顏色的點中深度最深的(貪心考慮,如果還有更深的,那麼選更深的一定更優)。那麼我們只要找出每種顏色深度最深的點,然後向該種顏色的每一個點暴力求距離即可。

由於所有顏色的點的個數加起來為n,總時間複雜度\(O(n\log n)\)

程式碼&題解

那麼對於多次路徑修改,僅一次查詢的問題,我們可以修改每個路徑時,在\(x\)處和\(y\)處打一個新增標記。然後在\(\operatorname{LCA}(x,y)\)處打2個刪除標記。(如果是對點修改,那就要在\(\operatorname{LCA}(x,y)\)\(\operatorname{fa}_{\operatorname{LCA}(x,y)}\)處各打1個刪除標記,因為LCA這個點要被算一次).最後自底向上累加標記。

[Codeforces 191C]給出一棵樹,再給出k條樹上的簡單路徑,求每條邊被不同的路徑覆蓋了多少次
在樹上,每個節點初始權值為0,對於每條路徑(x,y),我們令節點x的權值+1,節點y的權值-1,節點LCA(x,y)的權值-2。最後進行一次DFS,求出F[x]表示x為根的子樹中各節點的權值之和,F[x]就是x與它的父節點之間的樹邊被覆蓋的次數

程式碼&題解

[BZOJ3307]雨天的尾巴
給出一棵N個點的樹,M次操作在鏈上加上某一種類別的物品,完成所有操作後,要求詢問每個點上最多物品的型別。N, M≤100000
對於每條鏈(x,y),我們在x,y打一個+標記,lca(x,y)和lca(x,y)的父親打一個-標記。然後在每個節點建立一棵權值線段樹,下標v維護物品v的個數。如果有物品v,就把下標為v的位置+1,如果有-標記,就-1.線段樹push_up的時候可以計算出最多物品的型別
然後從下往上線段樹合併,合併到某個節點的時候就更新該節點的答案。

程式碼&題解

樹上倍增

樹上倍增可以用來\(O(n\log n)\)預處理,\(O(\log n)\)維護樹上的路徑資訊.設\(anc_{i,j}\)表示節點\(i\)向上\(2^j\)深度走到的節點,\(f_{i,j}\)表示節點\(i\)向上\(2^j\)深度這條路徑上,我們要維護的資訊。
那麼:
\(anc_{i,j}=anc_{anc_{i,j-1},j-1}\)

\(f_{i,j}=merge(f_{i,j-1},f_{anc_{i,j-1},j-1})\)

其中\(merge\)表示合併兩個答案,比如求\(\max\)
這是因為向上\(2^j\)的路徑可以拆成長度為\(2^{j-1}\)的兩段,\(i \to anc_{i,j-1}\)\(anc_{i,j-1} \to anc_{i,j}\)

初始值\(anc_{x,0}=\operatorname{fa}_x\),\(f_{x,0}\)就代表\(x\)到父親的邊的權值,這樣遞推即可。複雜度\(O(n\log n)\).

樹上倍增求LCA

先樹上倍增維護出\(anc\)陣列。考慮如何查詢。我們從\(x\)\(y\)中先選一個較深的點,然後將兩個點跳到同一深度。跳的過程類似二進位制拆分,從\(\log_2n\)\(0\)列舉\(i\),嘗試向上跳\(2^i\)步,如果跳的太多\(deep_{anc_{x,i}} < y\),就不跳,否則向上跳。此時有可能\(x,y\)重合,說明\(x,y\)是祖先後代關係,直接返回。否則一起從大到小往上跳,保證\(anc_{x,i}=anc_{y,i}\)

int lca(int x,int y){
    if(deep[x]<deep[y]) swap(x,y);
    for(int i=log2n;i>=0;i--){
        if(deep[anc[x][i]]>=deep[y]){
            x=anc[x][i];
        }
    }
    if(x==y) return x;
    for(int i=log2n;i>=0;i--){
        if(anc[x][i]!=anc[y][i]){
            x=anc[x][i];
            y=anc[y][i];
        }
    }
    return anc[x][0];
}

對於樹上資訊的查詢,過程和LCA類似,比如維護樹上邊權和最大值

long long lca_query(int x,int y){
    if(deep[x]>deep[y]) swap(x,y);
    long long maxl=0;
    for(int i=log2n;i>=0;i--){//先將x和y調整到同一深度
        if(deep[fa[y][i]]>=deep[x]){
            maxl=max(maxl,mlen[y][i]);//y上升同時更新maxl
            y=fa[y][i];
        }
    }
    if(x==y) return maxl;//如果LCA(x,y)=x,直接返回
    for(int i=log2n;i>=0;i--){//x,y同時上升,直到差一條邊相遇
        if(fa[x][i]!=fa[y][i]){
            maxl=max(maxl,max(mlen[x][i],mlen[y][i]));
            x=fa[x][i];
            y=fa[y][i];
        }
    }
    maxl=max(maxl,max(mlen[x][0],mlen[y][0]));//最後再更新一次
    return maxl;
}

[Codeforces 609E]
給定一個無向連通帶權圖G,對於每條邊(u,v,w),求包含這條邊的生成樹大小的最小值
先求出整張圖的最小生成樹大小tlen,對於每一條邊(u,v,w),我們最小生成樹中去掉樹上從u到v的路徑上權值最大,最大值為mlen的一條邊,再加上w,得到的一定是包含這條邊的生成樹大小的最小值tlen−mlen+w

樹上倍增維護最大邊權即可.
程式碼&題解

[HNOI2016]樹
給出一棵n個點的模板樹和大樹,根為1,初始的時候大樹和模板樹相同。接下來操作m次,每次從模板樹裡取出一棵子樹,把它作為新樹裡節點y的兒子。操作完之後有q個詢問,詢問新樹上兩點之間的距離\(n,m,q \leq 1 \times 10^5\)
顯然直接把所有節點存下來是不行的,因為節點的個數最多可以到\(10^{10}\)。發現本質不同的子樹只有n個,我們考慮把子樹縮成一個點,構造一棵新樹。

新樹上的點有3種編號,注意區分:

​1.小節點的詢問編號,即圖中淡黑色數字(1~9),詢問和加點的時候被輸入,可能會爆int
​2.所在大節點編號,即圖中加粗的黑色數字
​3.在模板樹上對應的點的編號,即圖中綠色數字

定義兩個大節點之間的邊權為大節點對應子樹的樹根和被掛上的節點在模板樹上的距離+1。每一個大節點需要儲存:

​1. 子樹內小節點詢問編號的範圍idl[x],idr[x]
​2. 根節點在模板樹上對應的點的編號from[x]
3. 這棵子樹接到的節點的詢問編號link[x]

然後寫兩個函式

  1. get_root(x) 找到詢問編號為x的節點所在大節點的編號。只需要二分答案,找到滿足idl[k]<=x的最小k即可
  2. get_tpid(x) 找到詢問編號為x的節點在模板樹上的編號。由於新加入大樹的結點是按照在模板樹中編號的順序重新編號,那麼他們的大小順序不變。我們先找到x所在大節點rt,再找到rt在模板樹上對應的點的編號from[rt],顯然答案是from[rt]的子樹中第x-idl[rt]+1小的節點編號。只需要在模板樹上按照dfs序建出主席樹,維護編號的出現情況即可。

然後在大節點構成的樹上進行樹上倍增,維護lca和x往上走2^i步的邊權和。

準備工作已經做完,我們來考慮如何求(x,y)距離

如圖,我們先加上模板樹上x到rtx的距離,然後在新樹上像求lca一樣往上跳,同時累計距離。直到x,y的父親相同。

但是這裡跳到最後一步的時候會出問題。如圖,我們求9到5的距離,從rty直接跳到rty的父親1會出問題,因為這樣並不是最短路徑,應該從rty跳到link[rty]才對,所以最後一步特判一下即可,x,y最後一步的距離為1+1+模板樹上link[x]到link[y]的距離,其中1表示從a跳到link[a]的距離為1

程式碼&題解

樹的遍歷

前序,中序,後序遍歷在這裡不再贅述.現在介紹兩種把樹上問題轉化為序列問題維護的方法。一般來說,用DFS序維護子樹,尤拉序維護路徑

DFS序

從根節點開始DFS,第一次訪問某節點的時候給它標號,每個節點的標號就是它的DFS序.代表DFS訪問的順序.

任意節點子樹內的DFS序是連續的
也就是說,把節點按DFS序排序後,\(x\)的子樹是一個連續區間,左端點為\(x\)的DFS序,右端點是\(x\)子樹內DFS序的最大值。那麼我們就可以把子樹問題轉化為區間問題,進而用線段樹等資料結構來維護。

int tim=0;
void dfs(int x,int fa){
    dfn[x]=++tim;//x的dfs序
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa) dfs(y,x);
    }
    dfnr[x]=tim;//記錄子樹內的最大DFS序
}

[SDOI2015]尋寶遊戲
小B最近正在玩一個尋寶遊戲,這個遊戲的地圖中有N個村莊和N-1條道路,並且任何兩個村莊之間有且僅有一條路徑可達。遊戲開始時,玩家可以任意選擇一個村莊,瞬間轉移到這個村莊,然後可以任意在地圖的道路上行走,若走到某個村莊中有寶物,則視為找到該村莊內的寶物,直到找到所有寶物並返回到最初轉移到的村莊為止。小B希望評測一下這個遊戲的難度,因此他需要知道玩家找到所有寶物需要行走的最短路程。但是這個遊戲中寶物經常變化,有時某個村莊中會突然出現寶物,有時某個村莊內的寶物會突然消失,因此小B需要不斷地更新資料,但是小B太懶了,不願意自己計算,因此他向你求助。為了簡化問題,我們認為最開始時所有村莊內均沒有寶物
本質上是在一棵樹上取出若干節點,詢問把這幾個節點訪問一遍的距離。可以發現如果我們按照dfs序將節點排序,然後將排序後的相鄰節點距離相加,最後再加上序列首尾距離,就能求出答案,如序列為{1,3,4,5},則答案為dist(1,3)+dist(3,4)+dist(4,5)+dist(5,1)。因為我們訪問節點一定是像dfs一樣訪問才能得到最短路徑,所以正確性顯然

我們用一個set維護這個排序後的節點序列,可以發現,每次新加入一個節點之後只會改變節點的前驅和後繼相關的距離,更新一下即可。這個問題也叫做樹鏈的並,是DFS序的經典應用。

程式碼&題解

[51nod 1681]公共祖先
給出兩棵n(n<=100000)個點的樹,對於所有點對求它們在兩棵樹中公共的公共祖先數量之和。
如圖,對於點對(2,4),它們在第一棵樹裡的公共祖先為{1,3,5},在第二棵樹裡的公共祖先為{1},因此公共的公共祖先數量為2
把所有點對的這個數量加起來,就得到了最終答案

\(O(n^3)\)的暴力不講了,先考慮\(O(n^2)\)的做法

列舉點對複雜度太高,不可行。我們考慮每個節點x作為公共的公共祖先的次數。設樹A上的節點x,在樹B上對應的節點是x'(實際上x'和x的編號是相同的,只是這樣方便描述).則如果點對既在x的子樹中,對應到B上後又在x'的子樹中,則這個點對的公共的公共祖先就包含x .注意一個小細節,如果x是y的父親,x不算做x和y的祖先,所以這裡的“子樹”應該不包含x.

如這張圖中,A中1的子樹中節點有{2,3,4,5},{2,3,4,5}對應到B中均在1的子樹內。這4個節點中任選一對,它們的公共祖先都包含1

那麼我們只要考慮x的子樹中有多少個點對應過去在樹B上x'的子樹中即可。暴力列舉x子樹中的每個節點,然後判斷。設這樣的點個數為cnt,則x作為公共的公共祖先的次數就是\(C_{cnt}^2\),把它累加進答案

那麼我們怎麼把它優化呢?我們發現,節點編號是離散的,不好判斷。但子樹中節點的dfs序是連續的。我們把A中節點x的dfs序標記到樹B上對應的位置x‘。然後我們遍歷樹A的每個節點x,它子樹的dfs序範圍為[l[x]+1,r[x]] (不包含x)。那麼問題就變成在樹B上編號為x的節點的子樹中有多少個節點的標記落在[l[x]+1,r[x]]的範圍內

如圖,我們想求A中3的子樹中有多少個節點對應到B中也在3的子樹裡,l[3]=2,r[3]=5,B中3的子樹中的dfs序有{2,4},落在[2+1,5]的範圍內的只有4,所以有1個節點

這是線段樹合併的經典問題。用權值線段樹合併就可以了,節點x的線段樹的節點\([l,r]\) 儲存有x的子樹中多少個值落在\([l,r]\)內。(有些題解用了可持久化線段樹,其實沒有必要)。我們遍歷的時候從下往上合併,合併到節點x的時候就更新x的cnt值。時間複雜度\(O(n\log n)\)

程式碼&題解

尤拉序

不同於DFS序.對有根樹進行深度優先遍歷,無論是遞迴還是回溯,每次到達一個節點就把編號記錄下來,得到一個長度為\(2n−1\)的序列,稱為樹的尤拉序列。

ST表+尤拉序求LCA

定理:記\(st_x\)表示\(x\)在尤拉序中第一次出現的位置,\(x\)\(y\)的LCA是尤拉序在區間\([st_x,st_y]\)的節點中深度最小的節點。
證明:
如果\(x\)\(y\)是祖先後代關係,顯然成立。否則要從\(x\)走到\(y\),要先DFS\(x\)的子樹,再回溯到\(lca(x,y)\)處,繼續DFS包含\(y\)的子樹。

那麼就可以查詢靜態區間最小值,用ST表可以做到\(O(n\log n)-O(1)\),用\(\plusmn1\)RMQ可以做到\(O(n)-O(1)\),但程式碼極其毒瘤。

int seq[maxn*2+5];//按尤拉序排序後的節點序列
int first[maxn+5];//即st
int deep[maxn*2+5];//深度
void dfs(int x,int fa,int d) {
    seq[++cnt]=x;
    deep[cnt]=d;
    first[x]=cnt;
    for(int i=head[x]; i; i=E[i].next) {
        int y=E[i].to;
        if(y!=fa) {
            dis[y]=dis[x]+E[i].len;
            dfs(y,x,d+1);
            seq[++cnt]=x;
            deep[cnt]=d;
        }
    }
}
struct ST {//ST表求lca
    int log2[maxn*2+5];
    int f[maxn*2+5][maxlogn+5];//維護區間深度最小的節點的位置
    void ini(int n) {
        log2[0]=-1;
        for(int i=1; i<=n; i++) log2[i]=log2[i>>1]+1;
        for(int i=1; i<=n; i++) f[i][0]=i;
        for(int j=1; (1<<j)<=n; j++) {
            for(int i=1; i+(1<<j)-1<=n; i++) {
                int x=f[i][j-1];
                int y=f[i+(1<<(j-1))][j-1];
                if(deep[x]<deep[y]) f[i][j]=x;
                else f[i][j]=y;
            }
        }
    }
    int query(int l,int r) {//返回位置
        int k=log2[r-l+1];
        int x=f[l][k];
        int y=f[r-(1<<k)+1][k];
        if(deep[x]<deep[y]) return x;
        else return y;
    }
} S;
inline int lca(int x,int y) {
    x=first[x];
    y=first[y];
    if(x>y) swap(x,y);
    return seq[S.query(x,y)];//返回最小值位置,再找到那個位置的編號
}

[51nod 1766]樹上的最遠點對
給出一棵N個點的樹,Q次詢問一點編號在區間[l1,r1]內,另一點編號在區間[l2,r2]內的所有點對距離最大值。\(N, Q≤100000\)
區間\([l,r]\)儲存編號在\([l,r]\)內的點組成的一棵樹的直徑端點和長度

考慮如何合併區間。,設兩個區間的直徑分別為(a,b),(c,d),則新區間的直徑端點肯定也是a,b,c,d中的一個。(運用直徑的性質3,4,那麼新區間的直徑就是\(\max(\operatorname{dist}(a,b),\operatorname{dist}(a,c),\operatorname{dist}(a,d),\operatorname{dist}(b,c),\operatorname{dist}(b,d),\operatorname{dist}(c,d))\)

那麼直接線段樹維護就行了,pushup的時候按上面的那樣合併。最後查詢得到[l1,r1]內的直徑(a,b),[l2,r2]內的直徑(c,d) ,答案就是\(\max(\operatorname{dist}(a,c),\operatorname{dist}(b,d),\operatorname{dist}(b,c),\operatorname{dist}(a,d))\)

如果用樹上倍增求lca,時間複雜度為\(O(n\log^2n)\),改用尤拉序+ST表求lca,查詢只需要在ST表中求最值,是O(1)的,時間複雜度\(O(n\log n)\)

程式碼&題解

樹鏈剖分

樹鏈剖分指的是把樹上的點劃分成一些鏈,然後用資料結構維護每條鏈。鏈上的邊稱作重邊,連線鏈之間的邊稱為輕邊。由多條重邊連線而成的路徑稱為重鏈,由多條輕邊連線而成的路徑稱為輕鏈。比如按子樹大小的輕重鏈剖分和長鏈剖分,還有LCT的實鏈剖分。(不同剖分方法對兩種邊的叫法不同,但本質相同)

輕重鏈剖分

對於每個點,我們把子樹大小\(sz\)最大的兒子稱為重兒子,記作\(son_x\)(為了避免混淆,所有的兒子集合記作\(child_x\)). 那麼我們把每個點和它的重兒子連成一條鏈,樹就被剖分成了很多鏈。記\(top_x\)表示\(x\)所在重鏈的頂端節點。

另外為了能用資料結構維護重鏈,我們優先DFS重節點,這樣重鏈的DFS序就是連續的一段,可以快速維護。比如\(x\)上方的重鏈的DFS序範圍就是\([dfn_{top_x},dfn_x]\)

int dfn[maxn+5];//點的DFS序
int fa[maxn+5];//點的父親
int son[maxn+5];//重兒子
int sz[maxn+5];//子樹大小
int deep[maxn+5];//深度
int top[maxn+5];//重鏈的頂端節點
void dfs1(int x,int f){
    fa[x]=f;
    sz[x]=1;
    deep[x]=deep[f]+1;
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=f){
            dfs1(y,x);
            sz[x]+=sz[y];
            if(sz[son[x]]<sz[y]) son[x]=y;//找到重兒子
        }
    }
}
int tim=0;
void dfs2(int x,int t){
    top[x]=t;
    dfn[x]=++tim;
    if(son[x]) dfs2(son[x],t);//優先DFS重兒子
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa[x]&&y!=son[x]){//對於輕兒子,以它為起點開始一條重鏈
            dfs2(y,y);
        }
    }
}

輕重鏈剖分有如下性質:

1.對於輕邊\((x,y)(deep_x<deep_y)\),有\(sz_x>2sz_y\).
因為\(y\)是輕兒子,所以必定存在另一個兒子的子樹大小\(>sz_y\),

2.任意節點\(x\)到根的路徑上輕邊個數為\(O(\log n)\)
根據性質1,每次往上跳一條輕邊,子樹大小會\(\times 2\),所以至多\(\log_2 n\)條。
換句話說,經過的重鏈最多\(O(\log n)\)

樹鏈剖分求LCA

我們先把\(x\),\(y\)中深度大的跳到重鏈頂端\(top_x\),再跳到下一條重鏈(即\(fa_{top_x}\)).這樣一直進行這個過程,直到\(x,y\)跳到同一條重鏈。此時因為在同一條重鏈上,深度淺的就是答案。

int lca(int x,int y){
    while(top[x]!=top[y]){
        if(deep[top[x]]>deep[top[y]]) x=fa[top[x]];
        else y=fa[top[y]];
    }
    return deep[x]<deep[y]?x:y;
}

維護路徑資訊

對於一條路徑\((x,y)\),我們也像求LCA那樣,輪流向上跳。這樣就把問題轉化成了\(2\log_2n\)個的區間修改和區間查詢。複雜度\(O(\log n)\)

void update(int x,int y,int v){
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        T.update(dfn[top[x]],dfn[x],v);
        x=fa[top[x]];
    }
    if(deep[x]>deep[y]) swap(x,y);
    T.update(dfn[x],dfn[y],v);
}
ll query(int x,int y){
    ll ans=0;
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        ans+=T.query(dfn[top[x]],dfn[x]);
        x=fa[top[x]];
    }
    if(deep[x]>deep[y]) swap(x,y);
    ans+=T.query(dfn[x],dfn[y]);
    return ans;
}

[LNOI2019]LCA
給出一棵N個點的樹,要求支援Q次詢問,每次詢問一個點z與編號為區間\([l,r]\)內的點分別求最近公共祖先得到的最近公共祖先深度和。N, Q≤50000
對於一個點i,我們把i到根節點的路徑全部標記+1,然後從z往上找,第一個碰到的標記不為0的節點就是\(\operatorname{LCA}(z,i)\)。而i的深度恰好就是z到根節點路徑上的標記和。顯然這樣的標記是可以疊加的,對於區間\([l,r]\),我們把編號在\([l,r]\)內的節點到根的路徑都標記+1,那麼答案就在z到根路徑上的標記和。

但是這樣直接做還是\(O(n^2)\)的,考慮離線。注意到標記是可減的,那麼詢問\(query(l,r,z)\)就相當於\(query(1,r,z)-query(1,l-1,z)\)

那麼我們分兩部分維護答案,記\(query(1,r,z)=ansr,query(1,l-1,z)=ansl\),真正的答案就是\(ansr-ansl\).我們對於每個點,儲存左端點l-1在此的詢問編號,右端點同理。我們從1~n遍歷每個節點i,把i到根的路徑標記+1。然後看看有沒有左端點在i的詢問,如果有,就更新ansl,右端點同理
程式碼&題解

此題還有一個加強版

[SPOJ2666]Query on a tree IV & [ZJOI2007]捉迷藏

給定一棵包含 N 個結點的樹,帶邊權且邊權可能為負。每個節點要麼是黑色(亮燈),要麼是白色(不亮燈)。初始時每個節點都是白色。
要求模擬兩種操作:(1)改變某個結點的顏色。(2)詢問最遠的兩個白色結點之間的距離。
\(N \leq 10^5\)
首先對樹進行輕重鏈剖分。對於每個節點,記\(d_1(x)\)\(d_2(x)\)分別表示該節點到子樹中的白色節點的最長距離和次長距離,且兩條路徑僅在根節點處相交.如果不存在,則記為\(- \infin\)

對於每條鏈上的節點,我們要維護以下三個變數:

  1. \(lmax\): \(x\)所在重鏈的最淺節點到\(x\)子樹中最遠白點的距離
  2. \(rmax\): \(x\)所重鏈的最深節點到\(x\)子樹中最遠白點的距離
  3. \(mlen\):與\(x\)所在重鏈相交的,\(x\)子樹中兩個白點中間的路徑的最長長度.

因為重鏈上節點的dfs序是連續的,那麼重鏈對應一個區間\([l,r]\),記\(id_{l}\)為dfs序為\(l\)的節點編號,最淺的節點為\(id_l\),最深的節點為\(id_r\)。因此我們可以對每條重鏈開一棵線段樹來維護這幾個變數。
\(dist(i,j)\)\(i,j\)間距離,\(p\)為區間\([l,r]\)對應的線段樹節點,\(lp,rp\)\(p\)的左右兒子。\(mid=\frac{l+r}{2}\),那麼有:

\[lmax(p)=\max(lmax(lp),dist(id_{l},id_{mid+1})+lmax(rp)) \]

第二項就是把rp對應的一個字首接到鏈\([l,mid]\)

\[rmax(p)=\max(rmax(rp),rmax(lp)+dist(id_{mid},id_r) \]

\[mlen(p)=\max(mlen(lp),mlen(rp),rmax(lp)+dist(id_{mid},id_{mid+1})+lmax(rp)) \]

由於是一條鏈,\(dist\)可以\(O(1)\)算出。直接線上段樹裡 push_up即可.

void push_up(int x) {
    int l=tree[x].l,r=tree[x].r,mid=(l+r)>>1;
    tree[x].lmax=max(tree[lson(x)].lmax,dist[hash_dfn[mid+1]]-dist[hash_dfn[l]]+tree[rson(x)].lmax);//注意線段樹是按dfs序存的
    tree[x].rmax=max(tree[rson(x)].rmax,dist[hash_dfn[r]]-dist[hash_dfn[mid]]+tree[lson(x)].rmax);
    tree[x].mlen=max(max(tree[lson(x)].mlen,tree[rson(x)].mlen),
                        tree[lson(x)].rmax+dist[hash_dfn[mid+1]]-dist[hash_dfn[mid]]+tree[rson(x)].lmax);
}

葉子節點的初始值可以這樣設定
\(id_p\)是黑點,有:
\(lmax(p)=rmax(p)=d_1(id_p)\)
\(mlen(p)=d_1(id_p)+d_2(id_p)\) 因為\(d_1,d_2\)保證了交點只有一個,它們可以接起來

\(id_p\)是白點,有
\(lmax(p)=rmax(p)=\max(d_1(id_p),0)\) (把自己作為路徑結尾,所以和0取max)
\(mlen(p)=\max(d_1(id_p)+d_2(id_p),d_1(id_p),0)\)
和 (可以把自己作為路徑結尾,也可以兩條路接在一起)

\(d_1\)\(d_2\)可以用一個支援插入和刪除任意元素的大根堆維護,可以用STL中的multiset實現.每個節點開一個這樣的資料結構\(h[x]\),儲存可能的路徑長度。 初始化的時候只需遍歷\(x\)的輕兒子\(y\),用下面一層的重鏈更新上面的答案,插入\(y\)\(lmax+dist(x,y)\)即可。因此建樹的時候一定要從深到淺建。

for(int i=head[x]; i; i=E[i].next) {
    int y=E[i].to;
    if(y!=fa[x]&&y!=son[x]){
        h[x].insert(tree[root[top[y]]].lmax+E[i].len);
        //累加下面一層重鏈的答案
    }
}

處理查詢:
類似\(d_1\)\(d_2\),我們維護一個全域性的multisetans儲存每條重鏈的答案(鏈頂lmax)。查詢的時候輸出最大值

處理修改:
修改是最複雜的部分。我們沿著\(x\)往上跳,修改每一條重鏈。

  1. 要刪除當前重鏈對上方重鏈的影響,對於鏈頂節點父親,我們在\(h\)中刪去當前鏈頂的lmax+dist.
  2. 修改線段樹。如果是在被修改節點的重鏈上,就找到該節點,否則找到重鏈的最深節點。由於下面的重鏈已經修改完,我們可以用下面重鏈更新當前的答案。所以我們要在堆裡插入新的\(lmax+dist\).然後求出新的\(d_1,d_2\)來更新\(lmax,rmax,mlen\).接著上推即可。
  3. \(ans\)裡刪除舊的答案(鏈頂lmax),插入新的答案

實際上,這個過程和動態DP的修改是類似的。
程式碼&題解

DSU on Tree

DSU on Tree(它和並查集(DisjointSetUnion,DSU)沒有關係))它能求解的問題一般是:沒有修改,對於樹上的每個點,求它的子樹內有多少滿足某種性質的節點。(也可以離線處理詢問)
一般來說,統計這些節點的暴力過程會用到資料結構(比如求值為v的節點個數需要一個桶),且每次統計完後要清空該資料結構。

考慮暴力,我們可以對每個子樹暴力統計這些點的個數,統計完後清空。複雜度\(O(n^2)\).但是之前數過的個數可以被其他點所用,比如求它的父親\(fa_x\)的答案時,如果\(x\)的答案沒有刪除,就不需要重新統計\(x\)的子樹裡的節點。但是如果每個節點都不清空影響,又會造成重複統計。DSU on Tree演算法利用輕重鏈剖分的性質在這兩者之間取得了平衡.

對於節點\(x\),我們執行以下步驟
\(solve(x)\):

  1. 對於輕兒子\(y\),遞迴下去\(solve(y)\),並刪除其影響
  2. 對於重兒子,遞迴下去處理\(solve(son_x)\)
  3. 統計輕兒子對當前點\(x\)答案的影響
  4. 更新\(x\)的答案
  5. 刪除第3步中輕兒子對\(x\)答案的影響

(偽)程式碼

void calc(int x,int type){//type=1加入,type=-1擦除
    //判斷x是否滿足該性質
    for(int y : E[x]){
        if(vis[y]||y==fa[x]) continue;//算過的就不用繼續
        calc(y,type);//遞迴下去計算輕兒子
    }
}
void dfs2(int x,int is_heavy){
    for(int y : E[x]){//1.遞迴求輕兒子的答案
        if(y!=fa[x]&&y!=son[x]){
            dfs2(y,0);
        }
    }
    if(son[x]){//2.遞迴求重兒子的答案
        dfs2(son[x],1);
        vis[son[x]]=1;//通過標記vis,使得重兒子的影響不會被擦除
    }
    calc(x,1);//3.統計輕兒子對x答案的影響
    vis[son[x]]=0;//這是因為若x不是父親的重兒子,它的所有兒子都要擦除,所以把vis設成0
    ans[x]=//4.更新x的答案
    if(!is_heavy) calc(x,-1);//5.如果x不是父親的重兒子,就擦除影響
}

正確性證明:第一次遞迴下去(1,2步)求子樹中其他點的答案,並且重兒子對答案的影響被保留。第二次遞迴(第3步)是統計輕兒子是求當前點的答案。,未被擦除的重兒子和現在統計的所有輕兒子加在一起,就構成了當前點的答案。因此能做到不重不漏。

複雜度證明:一個節點被統計的的次數等於他到根節點路徑上的輕邊數+1. 這是因為只有在每個輕邊的上端會執行第3步統計輕兒子,統計到該節點。+1是我們的solve過程會遍歷每個點一次求答案。又因為我們在輕重鏈剖分中證明了任意節點到根的路徑上輕邊個數為\(O(\log n)\), 因此總複雜度為\(O(n\log n)\).

[Codeforces600E]一棵樹有n個結點,每個結點都是一種顏色,每個顏色有一個編號,求樹中每個子樹的最多的顏色(可能有多個)編號的和。\(n \leq 10^5\)
考慮如何統計答案,即calc函式如何寫。開一個桶儲存每個顏色出現的次數。再開一個桶維護每個出現次數對應的和,同時記錄top表示最大出現次數。如果在插入和刪除某個顏色之後,某個出現次數的和變成了0,就減少top。如果新增了某個出現次數,就增加top.

void calc(int x,int type){//type=1加入,type=-1擦除 
    sum[cnt[a[x]]]-=a[x];
    cnt[a[x]]+=type;
    sum[cnt[a[x]]]+=a[x];
    if(sum[top+1]) top++;
    else if(top>0&&sum[top]==0) top--;
    //答案就是sum[top]
    for(int y : E[x]){
        if(vis[y]||y==fa[x]) continue;
        calc(y,type);
    }
}

然後套上面DSU on Tree的板子即可。

程式碼

[Codeforces 208E] Blood Cousins
給出一個有根樹森林,點集總大小為\(n\)。有\(m\)個詢問,每個詢問包含兩個數\(v_i,p_i\),詢問\(v_i\)\(p_i\)級祖先的子樹內有多少和\(v_i\)深度相同
可以對每個深度維護一個按DFS序排的節點序列,然後二分查找出對應的區間。考慮DSU on Tree如何處理

先把詢問離線。相同深度點的個數轉化成:詢問x子樹內深度為k的個數-1.直接套dsu on tree板子,用桶來統計深度。k級祖先用倍增求。

程式碼&題解

長鏈剖分

長鏈剖分可以把維護子樹中只與深度有關的資訊。對於每個點,我們找子樹內最大深度最大的兒子作為重兒子。那麼我們就把樹剖分成了"長鏈",意義和重鏈類似。

int maxl[maxn+5];//子樹內最大深度
int son[maxn+5];
void dfs1(int x,int fa){
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa){
            dfs1(y,x);
            if(maxl[y]>maxl[son[x]]) son[x]=y;
        }
    }
    maxl[x]=maxl[son[x]]+1;
}

長鏈剖分有以下性質:

1.所有鏈長之和為\(O(n)\)
所有點僅會被恰好一條長鏈包含。

2.任意一個點\(x\)\(k\)次祖先\(y\)所在長鏈長度\(\geq k\)
\(x\)\(y\)的路徑被長鏈包含,那麼它的長度大於路徑長度\(k\)
若有輕鏈,那麼長鏈的深度肯定\(\geq k\),否則以這條鏈為長鏈更優

3.任意一個點向上跳重鏈的次數不會超過\(O(\sqrt n)\)
每次從重鏈跳到另外一條重鏈,新的重鏈長度至少加1.又因為所有鏈長之和是\(O(n)\),那麼在最壞情況下,重鏈長度分別為\(1,2,3 \dots \sqrt{n}\). 因此用長鏈剖分來解決LCA和樹上路徑查詢問題,複雜度不是很優秀。

雖然不能高效處理路徑問題,長鏈剖分可以優化以深度為下標的樹形DP。這是因為每個點子樹裡的深度範圍不是\([1,n]\)而是\([1,maxl_x]\),再根據長鏈剖分的性質就可以保證複雜度。思路和DSU on Tree類似,每次先繼承重兒子的資訊,然後再加上輕兒子。因為每個點僅屬於一條長鏈,且一條長鏈只會在鏈頂位置作為輕兒子暴力合併一次,所以時間複雜度\(O(n)\)。在\(O(1)\)繼承重兒子資訊這點上有不同的實現方式,一個巧妙的方法是利用指標實現.

[Codeforces 1009F]
給出一棵樹,設\(f_{i,j}\)表示i的子樹中距離點i距離為j的點的個數,現在對於每個點i要求出使得\(f_{i,j}\) 取得最大值的那個j。
顯然能寫出DP,\(dp_{x,j}\)表示距離為\(j\)的點的個數,則\(dp_{x,j}=\sum_{y \in child(x)} f_{y,j-1}\). 再掃一遍DP陣列就能得出最大\(j\). 鏈上資訊用指標實現\(O(1)\)轉移,鏈與鏈之間直接暴力合併。

int ini[maxn+5];
int *id=ini;//把長度n的記憶體初始化為0,然後分給各個重鏈,因為重兒子暴力繼承了之後只有輕兒子位置需要新增DP陣列,答案為sum(maxl[x])
int *dp[maxn+5];
int ans[maxn+5];
void dfs2(int x,int fa){
    dp[x][0]=1;
    if(son[x]){
        dp[son[x]]=dp[x]+1;//通過指標,讓dp[son[x]][j]直接和dp[x][j+1]同步(因為是兒子,距離少了1),這樣就不用複製
        dfs2(son[x],x);
        ans[x]=ans[son[x]]+1;
    }
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa&&y!=son[x]){
            dp[y]=id;
            id+=maxl[y];//給y分配maxl的空間,因為重鏈長度之和為O(n),所以正好在ini的範圍裡
            dfs2(y,x);
            for(int j=1;j<=maxl[y];j++){
                dp[x][j]+=dp[y][j-1];
                if(dp[x][j]>dp[x][ans[x]]) ans[x]=j;
                else if(dp[x][j]==dp[x][ans[x]]&&j<ans[x]) ans[x]=j;
            }
        }
    }
    if(dp[x][ans[x]]==1) ans[x]=0;
}

程式碼

虛樹

虛樹可以解決一類特殊的樹形DP問題。每次詢問給出\(n\)個點的樹上的一個點集\(S\),且保證\(\sum |S| \leq n\),求這些點組成的樹上的一些資訊。如果把\(S\)中任意兩點之間的所有點建出來,然後跑樹形DP,每次詢問的複雜度可以達到\(O(n)\).

虛樹演算法可以在\(O(|S|\log |S|)\)的時間複雜度下構建出一棵樹,樹上的點只包含詢問點和詢問點的LCA.這樣相當於對原樹進行了壓縮,壓縮後的樹就稱為虛樹,虛樹的規模不會超過\(2|S|\).

虛樹的構造演算法是一個增量演算法。我們先把所有點按DFS序排序,然後依次插入。插入的時候需要用到一個棧\(s\),棧中儲存上一次插入的點到根節點的鏈上的虛樹節點(這些虛樹節點之間的邊還沒被構建),按深度排序,棧頂深度最大。我們考慮當前點\(x\)和棧中鏈的關係:

  1. \(\operatorname{LCA}(s_{top},x)=s_{top}\),說明\(x\)會接到這條鏈下方,此時向棧里加入\(x\),這次插入結束
  2. \(\operatorname{LCA}(s_{top},x) \neq s_{top}\),說明\(x\)不在原來鏈上,出現了一條支鏈。而我們按DFS序排序,就說明不會有節點再接到棧中\(\operatorname{LCA}(s_{top},x) \to s_{top}\)這條鏈下方。於是將滿足\(deep_{s_{top-1}} \geq deep_{\operatorname{LCA}(s_{top},x)}\)的棧中節點兩兩彈出,加邊\((s_{top-1},s_{top})\).彈出完畢後原樹又恢復成了一條鏈,加邊\((s_{top},\operatorname{LCA}(s_{top},x)\)(若恰好等於新的\(s_{top}\),就不用加). \(s_{top}\)已經加完了邊,我們可以直接將棧頂設為LCA,然後加入\(x\)

最後棧中還剩一條鏈沒加,再按順序給這些點加邊

void insert(int x){
    if(top<=1){//1或2個點的特判
        s[++top]=x;
        return;
    }
    int lc=lca(x,s[top]);
    if(lc==s[top]){//情況1
        s[++top]=x;
        return;
    }
    //情況2
    while(top>1&&deep[s[top-1]]>=deep[lc]){
        T2.add_edge(s[top-1],s[top]);
        top--;
    }
    if(s[top]!=lc) T2.add_edge(lc,s[top]);
    s[top]=lc;//把棧頂
    s[++top]=x;
}
int cmp(int x,int y){
    return dfn[x]<dfn[y];
}
void solve(int *in,int k){
    sort(in+1,in+1+k,cmp);//按dfs序排序
    int root=lca(in[1],in[2]);//找出虛樹的根
    for(int i=3;i<=k;i++) root=lca(root,in[i]);
    top=0;
    s[++top]=root;
    for(int i=1;i<=k;i++){//依次插入
        if(in[i]!=root)insert(in[i]);
    }
    while(top>1){//對於棧中剩下的點加邊
        T2.add_edge(s[top-1],s[top]);
        top--;
    }
}

[BZOJ3611]大工程
國家有一個大工程,要給一個非常大的交通網路裡建一些新的通道。我們這個國家位置非常特殊,可以看成是一個單位邊權的樹,城市位於頂點上。在 2 個國家 a,b 之間建一條新通道需要的代價為樹上 a,b 的最短路徑。現在國家有很多個計劃,每個計劃都是這樣,我們選中了 k 個點,然後在它們兩兩之間 新建 \(\rm{C}_{k}^2\)條 新通道。現在對於每個計劃,我們想知道:
1.這些新通道的代價和
2.這些新通道中代價最小的是多少
3.這些新通道中代價最大的是多少
顯然需要建虛樹,我們考慮如何在虛樹上DP(注意:以下定義的點和子樹都是虛樹上的)

\(sz_x\)表示x的子樹內有多少個詢問點,\(sum_x\)表示x子樹內的路徑長度和,\(dmin_x\)表示x子樹內到x的最小路徑長度,\(dmax_x\)表示 x子樹內到x的最大路徑長度。對於x的子節點y,我們可以寫出如下的狀態轉移方程
\(sum_x=\sum_{y \in child(x)} sum_y+(k-sz_y) · sz_y ·dist(x,y)\)
\(dmin_x=\min_{y \in child(x)}(dmin_x,dmin_y+dist(x,y))\)
\(dmax_x=\max_{y \in child(x)}(dmax_x,dmax_y+dist(x,y))\)

其中\(dist(x,y)\)表示原樹上\(x\)\(y\)的距離。合併的時候用\(dmin_x+dmin_y+dist(x,y)\)去更新最小值,其餘同理。
程式碼&題解

樹分治

點分治

樹鏈剖分主要處理書上單條路徑的查詢,而點分治可以用來統計樹上滿足一定條件的所有簡單路徑數量。

既然是分治,我們每次選擇一個點,計算出經過這個點的所有路徑。然後遞迴下去處理去掉這個點之後的所有子樹。如果我們每一次都選擇當前子樹的重心,遞迴下去的子樹大小一定不超過原來的一半。因此遞迴層數不會超過\(O(\log n)\).

再考慮如何計算出經過這個點的所有路徑。我們先對\(x\)的子樹\(y\)DFS,統計到\(y\)的路徑,然後類似樹形DP合併兩棵子樹,將兩個子樹接起來得到經過\(x\)的路徑.在合併時要注意去掉路徑兩端在同一個子樹內的情況。如果合併的複雜度為\(O(sz_y)\),結合遞迴層數,總複雜度\(O(n\log n)\).如果合併過程需要用到二分查詢等,總複雜度\(O(n\log^2n)\). 如果合併過程的複雜度和

每題的計算方式都有所不同,需要具體問題具體分析。

void solve(int x){
    vis[x]=1;
    calc(x,0,1);//計算以x為根的答案
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(!vis[y]){
            calc(y,1,-1);//容斥,減去一條邊經過兩次的答案
            root=0;
            tot_sz=sz[y];
            get_root(y,0);
            solve(root);
        }
    }
}

[BZOJ3697]採藥人的路徑
採藥人的藥田是一個樹狀結構,每條路徑上都種植著同種藥材。採藥人以自己對藥材獨到的見解,對每種藥材進行了分類。大致分為兩類,一種是陰性的,一種是陽性的。採藥人每天都要進行採藥活動。他選擇的路徑是很有講究的,他認為陰陽平衡是很重要的,所以他走的一定是兩種藥材數目相等的路徑。採藥工作是很辛苦的,所以他希望他選出的路徑中有一個可以作為休息站的節點(不包括起點和終點),滿足起點到休息站和休息站到終點的路徑也是陰陽平衡的。他想知道他一共可以選擇多少種不同的路徑
設陰性和陽性的邊權分別為1和-1,那麼平衡就是路徑長度為0

考慮點分治的時候dfs每一顆分治子樹,用\(mark_i\)來標記當前節點到分治中心的路徑上距離的出現情況,如果\(mark_{deep_x}=1\),就說明\(x\)與某個祖先到中心出現過一個相同的距離,也就是說這一段上一定存在休息點。

然後考慮合併分治中心出來的若干子樹。設\(g_{0/1,i}\)表示當前子樹中子樹中距離為i的路徑數目,0/1表示是否有休息點。合併的時候類似樹形dp,為了防止一個子樹內重複合併,要再定義一個數組\(f_{0/1,i}\)表示當前已經合併過子樹中距離為i的路徑數目.每新來一個子樹,先dfs一遍算出g,再把g和f合併。
每棵子樹對答案的貢獻是

\[\begin{aligned}&g_{0,0} \times (f_{0,0}-1) \text{(兩條路徑都平衡,分治中心是休息站,-1是去掉路徑經過點數為0的情況)} \\ &+ \sum_{i}g_{1,-i} \times f_{1,i}+ g_{0,-i}\times f_{1,i}+ g_{1,-i}\times f_{0,i} \text{(兩條路徑和為0,平衡,且至少一條路徑上存在休息站)}\end{aligned} \]

實現上要注意負下標的問題,可以通過過載[]運算子來避免

程式碼&題解

[BZOJ3451]Normal
給你一棵 n個點的樹,對這棵樹進行隨機點分治,每次隨機一個點作為分治中心。定義消耗時間為每層分治的子樹大小之和,求消耗時間的期望。
此題考察了對點分治的理解。根據期望的線性性,答案是\(\sum_{i=1}^n(i的期望子樹大小)=\sum_{i=1}^n \sum_{j=1}^n [j在i的點分治子樹內]\)

考慮j在i的點分治子樹內的條件,顯然i到j的路徑上的所有點中,i是第一個被選擇為分治中心的。否則如果選的點不是i,那麼i和j會被分到兩棵子樹中。第一個被選擇的的概率是\(\frac{1}{dist(i,j)+1}\)(\(dist(i,j)\)表示i到j的距離)。那麼上式就可以寫成\(\sum_{i=1}^n \sum_{j=1}^n \frac{1}{dist(i,j)+1}\)

轉換一下,設\(cnt[d]\)表示\(dist(i,j)=d\)\((i,j)\)個數,那麼答案為\(\sum_{d=0}^{n-1} \frac{cnt[d]}{d+1}\)。考慮如何求\(cnt[k]\)

我們在點分治的過程中,dfs出深度為i的節點個數cd[i]。那麼求經過根節點的答案的時候就是\(cnt[i]=\sum_{j=0}^i cd[j]cd[i-j]\).容易看出這是一個卷積的形式,直接用cd和自身FFT求卷積即可。注意最後要像一般的點分治一樣容斥一下.

時間複雜度滿足遞推式\(T(n)=2T(\frac{n}{2})+\frac{1}{2}n\log n\).根據主定理答案是\(\Theta (n\log^2 n)\)

程式碼&題解

動態點分治

咕咕咕~