長鏈剖分隨想

  之前寫了那麼長一篇Blog…現在不如寫篇小短文…說一下另一種樹鏈剖分方法——長鏈剖分的事情。它可以比重鏈剖分更快地完成一些東西。

  樹鏈剖分的原始版本重鏈剖分非常經典,這裏就不從頭介紹了。

  原本的剖分方法是按照子樹大小剖分,與子樹點數最多的兒子連成鏈,所以叫做重鏈剖分…然後顯然就有一個點到根的路徑上至多$O(\log n)$條輕邊這個性質(因爲沿着輕邊走,每次子樹大小一定小於父親的一半)。有了這個性質就可以做各種路徑相關的查詢,暴力每次跳到重鏈開頭就好…

  而在一些問題裏,有這麼一種奇妙的剖分方式可以取得更好的效果。那就是按照子樹深度剖分,與最深的兒子連成鏈。之前一直不知道這個應該怎麼叫,直到冬令營上聽到敦敦敦提到“長鏈剖分”這個詞,我才知道這個應該這麼叫…

  我所知道的長鏈剖分才能做的應用有兩個,一個是$O(n)$統計每個點子樹中可合併的以深度爲下標的信息;另一個是經過一些預處理,單次$O(1)$在線查詢一個點的$k$級祖先。

 

  先說一下第一個:$O(n)$統計每個點子樹中可合併的以深度爲下標的信息。(如某深度的點數,點權和,最值)

  暴力的做法是$O(n^2)$的,因爲一個點的多個子樹的信息我們無法快速合併,合併複雜度可以達到$O(n)$。

  但是我們對於重鏈剖分的方法可以想出一個$O(n \log n)$的方法:自底向上統計,對於每個點,讓它繼承自己的重兒子的信息,然後我們暴力遍歷其它子樹並統計信息。這樣做的話,每個點會在它到根路徑上的$O(\log n)$條輕邊被計算的時候被遍歷,所以總複雜度是$O(n \log n)$的。

  看起來這個已經很優了,而且我們也用上了輕邊數量這個性質,感覺沒有浪費什麼東西。再想想的話可以發現,其中遍歷其它子樹這一步有點浪費,因爲我們統計的是可合併的以深度爲下標的東西,我們其實只要循環一遍其他子樹已經統計出來的信息就好了,這樣我們的代價就不是子樹大小而是深度了。但是這樣還是不夠的,複雜度沒有變化。不過我們注意到繼承重兒子這一點現在看起來就不是那麼完美了,因爲我們只需要深度爲下標的信息,但是重鏈剖分是按照點數爲標準的,所以我們可能繼承了一個連出很多點但是深度很淺的掃把形重兒子,而其它輕兒子雖然點數不多,但是可能深度反而更深,所以可能選一個輕兒子更優。所以我們改變策略,選擇繼承子樹最深的兒子的信息,然後循環其它子樹的深度把信息統計到這個點上。

  這樣的複雜度是什麼呢?如果我們仍然按照上面的方法分析,我們發現我們的複雜度可能不太對,因爲到根路徑上的輕邊數量不再有保證了。但是如果我們換一種方法考慮就可以得到一個很好的複雜度。我們考慮每個子樹被作爲輕兒子暴力統計的代價,代價是它的深度,而它的深度其實就是它爲頂端的長鏈的長度。每個點都是一個長鏈的開頭,而所有長鏈都是不相交的,也就是說所有子樹被作爲輕兒子暴力統計的代價和是$O(n)$的。而被作爲重兒子統計的代價,因爲父親直接繼承了它的數組,所以每個點是$O(1)$的。於是我們就可以用$O(n)$的複雜度統計一棵樹的每棵子樹內的可合併的以深度爲下標的信息。

 

  然後是第二個:經過一些預處理,單次$O(1)$在線查詢一個點的$k$級祖先

  先說一下別的做法…可以離線的話,我們顯然有一個非常水的總時間$O(n+q)$的單次詢問$O(1)$的做法…DFS過程中直接找棧裏的某一個即可。

  不能離線的話也有一些傳統做法。比如重鏈剖分,還是根據$O(\log n)$條輕鏈的性質,如果$k$級組先就在當前重鏈上則直接找到,否則往上一條重鏈跳。複雜度$O(\log n)$

  還有一種就是樹上倍增,預處理出每個點的$2^0,2^1,2^2,...,2^{\log n}$級祖先,然後詢問的時候我們考慮$k$的二進制表示,每次往爲1的那些位的祖先上跳(可以看作用若干個$2^i$的和表示$k$)。這樣預處理的複雜度是$O(n \log n)$,詢問的複雜度是$O(\log n)$。總體上比重鏈剖分還差。

  還有另外一個相比起來複雜度比較糟糕的做法,就是記錄一個點往上的前$\sqrt{n}$級祖先,詢問的時候暴力往上跳,顯然預處理的複雜度是$O(n \sqrt{n})$,單次詢問$O(\sqrt{n})$。

  如果我們想突破到單次詢問$O(1)$,我們依靠不了重鏈剖分這種經典做法。它對於它的思想來說已經相當優了,沒有什麼改進的空間。樹上倍增的特點是,可以一步跳很遠,但是如果要精準地跳到第$k$級,就一定要遍歷$k$的每一個二進制位。而我們想出第三種做法的基本思路是,如果我們對每個點都維護它的所有祖先,我們就能$O(1)$回答詢問。但是實際上$O(n^2)$的空間複雜度和預處理複雜度是不能承受的,於是我們折中地選擇根號。

  注意到如果我們查詢的$k$大於很多個$\sqrt{n}$的話,我們是一步一步跳$\sqrt{n}$級跳上去的,效率很低。這時我們其實可以考慮用樹上倍增來優化。樹上倍增要精準跳到$k$級祖先複雜度比較高的問題可以用第三種做法的特點來彌補。如果$k$在$\sqrt{n}$以內的話,我們可以$O(1)$跳過去。這樣做的結果是什麼呢?其實不太好,我們花了高昂的$O(n \sqrt{n})$的代價預處理,得到的結果僅僅是查詢時我們不再需要遍歷最低的幾個二進制位。

  但是這種思想還是可以繼續沿用的,接下來就是長鏈剖分出場的時候了。我們的目的是,讓樹上倍增進行儘量少的跳躍後就可以通過其他信息找到$k$級祖先。我們首先可以像重鏈剖分一樣維護一下每條長鏈,然後我們往上跑,求出長鏈頂端往上長鏈長度這麼多級的祖先。這樣做的時空複雜度仍然是$O(n)$。這樣有什麼用?這樣做以後,我們的樹上倍增只用跳最大的一步。

  顯然這個最大的一步的長度必定大於$k/2$,於是我們跳到的那個點往下的長鏈長度至少就有$k/2$,所以就算$k$級祖先不在這條長鏈上,也一定可以從我們跳到的那個點的已知信息裏直接求到(因爲剩下的步數已經小於$k/2$了,預處理的祖先長度$=$往下的長鏈長度$>k/2$)。於是我們只要再預處理出對於每個數,它最大的二進制位是多少,我們就可以$O(1)$地求出任意一個點的$k$級祖先了。(不過樹上倍增的預處理複雜度仍然是$O(n \log n)$,所以只有詢問個數很多的時候纔能有明顯的效果)

 

  我所知道的長鏈剖分的應用只有這兩個…如果有人知道什麼其它不用長鏈剖分的做法,或者長鏈剖分的其他的神奇應用的話,歡迎在下面留言。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章