點分治與點分樹學習

關於樹鏈分治的一些東西

《分治算法在樹的路徑問題中的應用》
在這裏插入圖片描述
例題
給出一棵n個結點的有根樹,每個結點有顏色。

有若干詢問,詢問有多少種顏色,在v爲根的子樹中至少有k個結點屬於該顏色。

算法1
莫隊(好像都是這麼叫的)離線方法。時間複雜度O(nn)O(n \sqrt n)

算法2
這個算法基於一個簡單的結論:每個詢問的答案不會超過n/kn/k。這樣,我們確定一個閥值x=n\sqrt n,當k≤x時的答案可以預處理出來,如果k>x,我們也要預處理,不過對於每個點,數量大於x的顏色最多隻有n/xn/x個,不會太多。
這樣的時間複雜度是O(nn)O(n \sqrt n)的,是一個在線算法。

算法3
離線算法,啓發式合併,時間複雜度O(n(logn)2)O(n(logn)^2)

算法4 重點
假設現在樹退化成了鏈,你會怎麼做呢?
當然是開兩個數組,num[i]表示i顏色出現的次數,cnt[i]表示k=i時的答案。這樣,我們從鏈尾到鏈頭掃一遍就可以O(n)解決了。

現在是樹,我們先把它剖成若干條鏈(其實是小於logn條),然後對於一條鏈,同樣採用上面的方法,只不過有個問題,這條鏈上可能帶有若干其他的鏈(如圖),這樣對於長度爲m的鏈,我們統計的複雜度並不是O(m)的。
在這裏插入圖片描述
1 2 3 4 這條鏈就連着一些其他鏈。

其實,我們直接暴力統計其他的鏈,由於每個點到根最多隻會遇到logn條鏈,所以每個點只會被拿去統計logn次。所以我們得到一個漂亮而簡潔的算法,時間複雜度O(nlogn)。
估計代碼非常短,速度非常快!

Part A.點分治

衆所周知,樹上分治算法有33種:點分治、邊分治、鏈分治(最後一個似乎就是樹鏈剖分),它們名字的不同是由於分治方式的不同的。點分治,顧名思義,每一次選擇一個點進行分治,對於樹上路徑統計類型的問題有奇效,思路很好理解,只是碼量有些煩人

先來看一道模板題:CF161D

至於爲什麼我沒有放Luogu模板題是因爲那道題只會寫O(n2logn)O(n^2logn)的算法(然而跑得過是因爲跑不滿)

這道題要求在NN個點的樹上找距離爲KK的點對的數量。

因爲我們是來學點分治的,所以我們考慮點分治。我們每一次選擇一個分治中心,那麼以這一個分治中心爲根,這棵樹就會有若干子樹。這棵樹上的路徑被分爲了兩種:

①經過分治中心

②沒有經過分治中心,也就是說這條路徑在以當前分治中心爲根的一棵子樹內

我們可以遞歸解決②對應的問題,也就是說我們只要解決當前樹的①問題。

考慮每一次選擇一棵子樹對其進行深度優先搜索,開一個桶記錄之前經過的子樹中每一種路徑長度對應的路徑數量(一個小注明:路徑指的是當前分治中心到達子樹中某一個點的路徑,下同)。每一次找到一條長度爲LL的路徑之後,它對答案的貢獻就是之前搜索過的子樹中長度爲KLK-L的路徑的數量,因爲這一條路徑可以與這一些路徑中的每一條拼接形成長度爲KK且經過當前分治中心的路徑。在一棵子樹遍歷完了之後,再將這一棵子樹的路徑放入桶內。注意:不能找到一條路徑就放進桶裏面,因爲這樣可能會導致同一棵子樹的兩條路徑被拼接並計入答案,但實際上它們之間的樹上路徑屬於②,不應該在當前分治中心被統計到。當前分治中心解決之後,清空桶中元素,分治解決以當前分治中心爲根的子樹上的路徑。

當然,你會發現一個問題:如果給出了一條鏈,結果你每一次選擇的分治中心都是鏈兩端的點,那複雜度不輕鬆卡成O(n2)O(n^2)???

然而智慧的你不會讓出題人這麼輕鬆地卡掉你,我們考慮每一次選擇一個點,以它爲根時,子樹大小盡量平均,也就是說最大的子樹要儘量的小

那麼我們當然會選擇——樹的重心!

因爲樹的重心的優雅性質(以它爲根的子樹的大小不超過當前樹大小的12\frac{1}{2}),我們每一次分治下去的子樹的大小都至少會減半,也就保證了O(nlogn)O(nlogn)的複雜度。

再來一題:Tree

咦這題的等於KK怎麼變成小於等於KK

那麼我們就不能使用桶了。而使用線段樹等數據結構碼量又會增大不少,我們可不可以用更優秀的方法解決呢?當然有。

每一次分治時,我們考慮將路徑存下來,並按照長度從小到大排序,然後使用兩個指針L,RL,R來掃描路徑數組並獲取答案。

可以知道,當LL在不斷向右移動的時候,滿足lenL+lenRKlen_L + len_R \leq K的最大的RR是單調遞減的,所以可以直接調整RR滿足要求。調整了RR之後,那麼我們的答案就是RLR-L

等等,我們沒有考慮同一子樹,所以我們還需要存下每一條路徑的來源是哪一棵子樹,用桶存好L+1L+1RR之間每一個來源的數量,每一次LLRR移動的時候維護這個桶,那麼實際貢獻的答案就是RLL+1到R中與L來源相同的路徑的數量R-L-\text{L+1到R中與L來源相同的路徑的數量}

我們每一次分治的複雜度就是O(分治區域大小log分治區域大小)O(\text{分治區域大小} log \text{分治區域大小})的,總複雜度是O(nlog2n)O(nlog^2n)。如果寫基數排序之類的東西的話複雜度就是O(nlogn)O(nlogn)

然後放幾道練習題:

基礎(比較裸就沒有講什麼了):

Luogu點分治模板

點分治模板
聰聰可可

聰聰可可
Race

Race
較難:(Solution更新中)

快遞員 Sol

樹的難題 Sol

樹上游戲

重建計劃 Sol

B.動態點分治(點分樹)

什麼?點分治還能帶修改?Of course!

我們可以發現:根據點分治,我們可以構建出一棵樹,在點分治過程中,如果從solve(a)solve(a)遞歸到了solve(b)solve(b),就令aa所在分治區域的重心爲bb所在分治區域重心的父親,這樣我們就可以構造出點分樹。點分樹有幾個優美的性質:

a.a.點分樹的高度不超過O(logn)O(logn),因爲點分治的遞歸深度不會超過lognlogn

b.b.點分樹上某個點的祖先(包括它自己)在點分治時的分治範圍必定包括了這個點,而其他點的分治範圍一定不會包含這個點。

c.c.點分樹上某個點的兒子一定在這一個點的分治範圍的子樹中(廢話)

這個性質告訴我們:如果在點分樹上進行修改,只需要修改它到根的一條鏈,修改點數不會多於lognlogn

具體來說,看一道題:捉迷藏 ; 加強版:Qtree4

我們就說Qtree4Qtree4的做法吧,畢竟捉迷藏是邊權爲11的特殊版本。

我們構建好點分樹,考慮如何在點分樹上維護答案。我們需要支持插入、刪除和查詢最大值、次大值,考慮使用堆+懶惰堆思想進行維護。

我們對每一個點維護一個堆heap1heap1,維護當前節點對應的分治範圍內的路徑的最大值和次大值。但我們又會面對與靜態點分治一樣的問題:可能來自當前節點的同一個子樹的一條路徑在當前節點貢獻答案。所以我們對於每一個節點還要維護當前節點對應的分治範圍內的路徑到達當前節點在點分樹上的父親的路徑長度的堆heap2heap2,這樣父親在轉移時就可以直接取它的所有兒子的heap2heap2的最大值放入自己對應的heap1heap1中,統計答案的時候把它的heap2heap2的最大值從它的父親的heap1heap1中刪掉,就可以避免了重複的計算。然後我們在全局維護一個堆heap3heap3來維護全局的答案,每一次產生新的答案就進行維護。

那麼我們每一次翻轉一個節點的顏色的時候,就在點分樹上暴跳父親,並維護好heap1,heap2,heap3heap1,heap2,heap3。初始化的時候也暴跳父親。複雜度O(nlog2n)O(nlog^2n),在Qtree4Qtree4上有一些卡常,給出一種優化方式:在刪除的時候,不要在懶惰堆中加入一個元素就嘗試刪除答案堆,而是在詢問的時候進行,這樣可以降低常數。

注意一個細節:如果某一個節點可以被計入路徑中,在它對應的heap2heap2中是需要插入兩個00的(表示自己與自己匹配或者自己與兒子匹配),這樣子的答案纔是正確的。

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