長鏈剖分也是一種樹鏈剖分,平時咱們說樹鏈剖分,通常都是直接默認爲輕重鏈剖分。
輕重鏈剖分的優秀性質在於從任意一個點開始,向上跳躍,跳過的重鏈數量不會超過\(log\)級別。
這樣子能夠很優秀的解決兩點之間鏈的問題。
對於解決一些子樹的信息問題,咱們能夠用\(dsu\ on\ tree\)的思路,保證了每一個點向上修改的次數不超過\(log\)次,也能夠很方便的解決一些問題。而長鏈剖分則是經過修改剖分鏈的方式,經過維護一些信息,能夠在更有優秀的時間中解決一部分問題。
長鏈剖分十分相似於輕重鏈剖分,可是咱們稍加修改,將每次選擇子樹大小最大的兒子做爲重兒子變成了選擇子樹深度最大的那個兒子做爲重兒子。而後將全部點和它的重兒子之間的邊認爲是重邊,若是咱們把他們在樹中所有加粗,那麼原樹就被分割成了若干條鏈。由於不少東西都和輕重鏈剖分是相同的,因此這一部分我寫的很簡單。甚至於連二者之間的代碼都是很是的類似的。html
void dfs1(int u,int ff) { md[u]=dep[u]=dep[ff]+1;fa[u]=ff; for(int i=h[u];i;i=e[i].next) { int v=e[i].v;if(v==ff)continue; dfs1(v,u); if(md[v]>md[hson[u]])hson[u]=v,md[u]=md[v]; } } void dfs2(int u,int tp) { top[u]=tp;len[u]=md[u]-dep[top[u]]+1; if(hson[u])dfs2(hson[u],tp); for(int i=h[u];i;i=e[i].next) if(e[i].v!=fa[u]&&e[i].v!=hson[u]) dfs2(e[i].v,e[i].v); }
稍微介紹一下每一個數組表示的含義分別是什麼。
\(dep\)是深度,\(fa\)是父親節點,\(md\)是\(maxdep\)也就是子樹中的最大深度
\(hson\)是重兒子,\(top\)是這條重鏈深度最小的點,也就是重鏈的頂點,\(len\)是重鏈的長度。
代碼應該仍是比較清楚地,因此就不在過多的解釋了。
若是沒有學太輕重鏈剖分,能夠先去學習一下,大概直接百度樹鏈剖分就行了。算法
性質很少,長鏈剖分的重點在於題目。可是全部題目的複雜度都和性質相關。數組
全部鏈長度的和是\(O(n)\)級別的。學習
證實:優化
全部點在且僅在一條重鏈之中,永遠只會被計算一次,由於鏈長的總和是\(O(n)\)級別的。spa
任意一個點的\(k\)次祖先\(y\)所在的長鏈的長度大於等於\(k\)指針
證實:
假如\(y\)所在的長鏈的長度小於\(k\),那麼它所在的鏈必定不是重鏈,由於\(y-x\) 這條鏈顯然更優,那麼\(y\)所在的重鏈長度至少爲\(k\),性質成立。
不然\(y\)因此在長鏈長度大於等於\(k\),性質成立。code
任何一個點向上跳躍重鏈的次數不會超過\(\sqrt n\)次htm
證實:
若是一個點\(x\)從一條重鏈跳到了另一條重鏈上,那麼跳躍到的這條重鏈的長度不會小於以前的重鏈長度。
那麼在最壞的狀況下,重鏈長度分別爲\(1,2,3,...,\sqrt n\),也就是最多跳躍\(\sqrt n\)次。
從這點上就能夠看出,若是用長鏈剖分來解決兩點之間的鏈以及\(LCA\)問題,複雜度是不優於樹鏈剖分的。blog
找\(k\)次祖先咱們有幾種方法,能夠樹鏈剖分以後跳重鏈,這樣是\(O(n)-O(logn)\)。
還能夠提早預處理好倍增數組,這樣是\(O(nlogn)-O(logn)\)的。
我以前還本身\(yy\)了一種辣雞作法,先樹鏈剖分再倍增,彷佛能夠作到\(O(nlogn)-O(loglogn)\)
還能夠對於每一個點,暴力維護\([1,\sqrt n]\)次祖先,這樣子是\(O(n\sqrt n)-O(\sqrt n)\)。
固然,若是支持離線的話,能夠作到\(O(n+q)\),只須要\(dfs\)的時候維護一個棧就行了。
然而這些都不夠優秀,咱們來考慮一下長鏈剖分的性質,看可否優化上述東西。
咱們再看看\(O(n\sqrt n)-O(\sqrt n)\)的作法,它能夠結合倍增,這樣的話,它的本質就是優化掉了很小的幾個二進制位。再回頭看看上面的第二條性質:\(k\)次祖先所在的重鏈長度不小於\(k\)。利用這個性質,咱們就能夠獲得一個很秒的方法了。咱們把\(k\)折半,假設是\(r\),不難發現\(r\)次祖先所在的重鏈長度不短於\(r\)。若是咱們提早維護出每條重鏈從上往下的每個點,以及對於每一個重鏈的頂端,維護它的重鏈長度個祖先,這樣子對於每次找到\(r\)次祖先以後,咱們就能夠經過一些判斷,找到\(k\)次祖先的位置,而最後這一次尋找不難發現能夠利用上面預處理出來的重鏈和祖先\(O(1)\)的計算。那麼如今的複雜度瓶頸又回到了找\(r\)次祖先。咱們直接找\(r\)次祖先真的優秀嗎?若是利用倍增尋找的話,仍然是\(O(logn)\)的複雜度。可是咱們發現\(r\)只須要知足\(r>k/2\)就能夠向上面那麼作了,所以咱們令\(r\)爲\(k\)的最高二進制位,也就是\(r=highbit(k)\),這樣子就能夠倍增預處理出來\(r\)倍祖先,而後\(O(1)\)找到\(k\)次祖先了。
補充一些複雜度以及空間的細節:根據性質一,對於每一個重鏈的頂點,維護整條鏈以及它小於鏈長次祖先,由於總的點數是\(O(n)\)級別的,因此這裏的時空負載度都是\(O(n)\)。\(highbit\)顯然能夠\(O(n)\)預處理,這樣子時空複雜度仍是\(O(n)\)。倍增的時空複雜度是\(O(nlogn)\),這個方法的複雜度瓶頸在此。
綜上所述,咱們獲得了一個預處理\(O(nlogn)\),單次詢問\(O(1)\)的方法。
這個思路十分相似於\(dsu\ on\ tree\),咱們長鏈剖分以後,每次不從新計算,所有繼承重兒子的值,而後再把其餘的全部輕兒子的貢獻額外的算進來。
直接這樣子說十分的不清晰,咱們找到題目來講。
由於會在這裏寫比較詳細的題解,因此上面寫得很\(Simple\)。
咱們先考慮一個\(O(n^2)\)的\(dp\),也就是原題的作法。
咱們考慮一下,三個點兩兩的距離相同是什麼狀況,
1.存在一個三個點公共的\(LCA\),因此咱們在\(LCA\)統計答案便可。
2.存在一個點,使得這個點到另外兩個子樹中距離它爲\(d\)的點以及這個點的\(d\)次祖先。
因此,設\(f[i][j]\)表示以\(i\)爲根的子樹中,距離當前點爲\(j\)的點數。
\(g[i][j]\)表示以\(i\)爲根的子樹中,兩個點到\(LCA\)的距離爲\(d\),而且他們的\(LCA\)到\(i\)的距離爲\(d-j\)的點對數。
考慮合併的時候的轉移:
\(ans+=g[i][0],ans+=g[i][j]*f[son][j-1],f[i][j]+=f[son][j-1],g[i][j]+=g[son][j+1]\)
轉移的正確性比較顯然,不在多講了,並非這裏的重點。
這樣子的複雜度是\(O(n^2)\)的。
咱們觀察一下轉移的時候有這樣兩步:\(f[i][j]+=f[son][j-1],g[i][j]+=g[son][j+1]\)
若是咱們欽定一個兒子的話,那麼這個數組是能夠直接賦值的,並不須要再重複計算。
因此咱們用指針來寫,也就是:\(f[i]=f[son]-1,g[i]=g[son]+1\)
若是整棵樹是鏈咱們發現複雜度能夠作到\(O(n)\),既然如此,咱們推廣到樹。
咱們進行長鏈剖分,每次欽定從重兒子直接轉移,那麼咱們還須要從輕兒子進行轉移。
不難證實全部輕兒子都是一條重鏈的頂部,轉移時的複雜度是重鏈長度。
那麼,複雜度拆分紅兩個部分:直接從重兒子轉移\(O(1)\),從輕兒子轉移\(O(\sum len)\)
發現每一個點有且僅有一個父親,所以一條重鏈算且僅被一個點暴力轉移,而每次轉移複雜度是鏈長。
因此全局複雜度是\(\sum\)鏈長,也就是\(O(n)\),所以總複雜度就是\(O(n)\)
這樣子寫下來,發現長鏈剖分以後,咱們的複雜度變爲了線性。
可是注意到複雜度證實中的一點:轉移和鏈長相關。
而鏈長和什麼相關呢?深度。因此說對於這一類與深度相關的、能夠快速合併的信息,使用長鏈剖分能夠優化到一個很是完美的複雜度。若是須要維護的與深度無關的信息的話,或許\(dsu\ on\ tree\)是一個更好的選擇。
這裏再額外補充一道題目,彷佛都比較簡單:(盡然找不到別的題目了)
題解啥的直接戳連接吧。
這題比較神仙,對於長鏈剖分的運用也是很妙的,思路能夠借鑑一下。
這裏再也不過細的討論這部份內容。
根據題目數量的分佈,也不難看出長鏈剖分咱們用的最多的仍然是維護和深度相關的信息。由於求\(k\)次祖先預處理的複雜度達到了\(O(nlogn)\),成爲了複雜度瓶頸,只有當詢問次數很是大的時候,才能體現出長鏈剖分\(O(1)\)回答的優越性。同理,維護一類貪心題,彷佛題目也比較固定,而且能夠用別的方法解決。只有在維護和深度相關的能夠快速合併的信息的時候,時間複雜度能夠作到\(O(n)\)級別,而包括輕重鏈剖分在內的一些算法通常都只能作到\(O(nlogn)\)級別,利用長鏈剖分能夠作到更大的數據範圍。
咕咕咕,這篇文章就到這裏了,主要是\(yyb\)太弱了,難題寫不動,就只能寫寫這些比較簡答的題目。