演算法學習 點分治

2022-03-29 06:13:36 字數 3758 閱讀 9238

【演算法梗概】

點分治,是一種針對可帶權樹上簡單路徑統計問題的演算法。本質上是一種帶優化的暴力,帶上一點容斥的感覺。

注意對於樹上路徑,並不要求這棵樹有根,即我們只需要對無根樹進行統計。接下來請把無根樹這一關鍵點牢記於心。

【引入】

話不多說,先看一題:

給定一棵樹,樹上的邊有權值,給定乙個閾值\(k\),請統計這棵樹上總長度小於等於\(k\)的路徑個數。

路徑長度為路徑路徑上所有邊的權值和。

這就是poj 1741。

題意描述很清楚,你是否已經有了想法?

考慮簡單的dfs過程,能否統計答案?

dfs把樹看作有根樹,那麼對於乙個子樹\(\mathfrak t\),根節點為\(t\),如何統計\(\mathfrak t\)中的路徑個數(答案)?

我們考慮\(\mathfrak t\)中的路徑。

把路徑分為兩種:

一、經過\(t\)的路徑。

二、不經過\(t\)的路徑。

這樣分類是顯然正確的,而且對於不經過\(t\)的路徑,它們一定在\(t\)的某個子節點所構成的子樹中。

這樣就對答案進行了劃分:樹\(\mathfrak t\)的答案等於\(\mathfrak t\)中經過\(t\)的路徑的答案加上\(t\)的所有子節點構成的子樹的答案

對於第二部分的答案,我們遞迴處理;現在考慮計算第一部分的答案,即計算經過\(t\)的路徑的個數。

這裡提供一種思路:

考慮路徑的合併,利用容斥去除非法路徑:

「路徑的合併」說的是\(t\)到\(\mathfrak t\)中的任意乙個節點(包括自身)的路徑集合的合併。

比如看下面這張圖:

現在樹根是\(a\)點,那麼路徑的集合是:\(\left\a\\a\to b\\a\to b\to d\\a\to b\to e\\a\to c\\a\to c\to f\end\right\}\)。

兩兩組合,共有\(c_^=c_^=21\)種不同的方式。但是顯然有不合法的路徑:比如\(a\to b\to d\;,\;a\to b\to e\)不能合併。

注意到一條路徑可以簡單地用路徑的終點表示,那麼共有子樹節點個數條路徑,而能夠合併的路徑只有在不同子樹的路徑,而在同一子樹的路徑無法合併。

當然,可以合併的路徑也可能因為路徑長度大於\(k\)而不能計入答案。

先不考慮非法的路徑,看看如何統計長度小於等於\(k\)的路徑:

那麼接下來考慮容斥去除非法的路徑,對於根節點的每個子節點代表的子樹,按照同樣方式統計答案,並把得出的結果從原來的答案中減去即可。

真的就行了嗎?

其實還要考慮子樹的路徑長度,剛才對根的計算時,子樹的所有路徑長都加上了根到子樹的邊的權值。

那麼在對這棵子樹計算時,注意子樹的路徑也都要加上這條邊的權值,這樣正好和原來的路徑長度吻合。

或者,同樣的道理,因為這條邊多計算了兩次,把\(k\)相應地減少\(2\)倍的這條邊的權值也可以。

那麼對於乙個節點,總的時間複雜度為\(o(n\;log\;n)\),\(n\)為子樹節點個數。

那麼這樣,就能對於乙個節點的子樹統計答案了,最終把所有答案加起來就行了。

但是,真的就行了嗎?請看下乙個內容。

【演算法核心】

可以看到,計算乙個節點的複雜度為\(o(n\;log\;n)\),但要保證總複雜度不超過乙個量級卻很難。

但是我們可以利用無根樹的性質!

可以看到,把乙個點的答案算完時,它的子節點所代表的子樹就互不影響了!

就是說,這些子樹彼此獨立,可以完全當作乙個新的子問題處理。

那麼考慮如下演算法:

對於這棵無根樹,找到乙個點,使得它在樹的中心位置,滿足如果以它為根,它的最大子樹大小盡量小,這個點稱為重心

以這個點為根,計算它的答案。

把以這個點為根的樹的所有子樹單獨作為乙個子問題,回到步驟\(1\)遞迴處理。

這個演算法的複雜度是多少呢?

先介紹乙個定理:以樹的重心為根的有根樹,最大子樹大小不超過\(\frac\)。

假設超過了,大小為\(k>\frac\),那麼其他子樹大小之和等於\(n-k-1\)。

那麼把重心往這個子樹方向移動,最大子樹大小一定減小,因為\(n-k

這樣,就進一步說明了演算法總時間複雜度不超過\(o(n\;log^2\;n)\)。

【演算法實現】

按照上述步驟實現**:

①計算重心位置:使用一次簡單的dfs來實現。

②計算答案:直接用另乙個dfs計算。

③分治子問題:重新呼叫尋找重心的dfs函式,再遞迴求解即可。

那麼可以根據此,寫出**:

void getroot(int u,int f)
這是尋找重心的函式,需要傳入父節點,還要呼叫vis陣列。呼叫前保證root等於0,並且wt[0]等於無限大。

注意第5行,tsiz是當前處理的樹的大小,這是因為把無根樹轉成有根樹後,父親所連的子樹也是自己的孩子了。

void dfs(int u,int d,int f)

int calc(int u,int d)

}

這是點分治的核心函式,傳入的是當前樹的重心,在呼叫時計算重心的答案。

然後求每個子樹的重心,再遞迴求解。

對於剛剛的題目,有如下**實現:

#include#include#include#define f(i,a,b) for(int i=a;i<=(b);++i)

#define ef(i,u) for(int i=h[u];i;i=nxt[i])

using namespace std;

const int inf=0x3f3f3f3f;

int n,k,ans;

int h[10001],nxt[20001],to[20001],w[20001],tot;

inline void ins(int x,int y,int z)

bool vis[10001];

int root,tsiz,siz[10001],wt[10001];

int arr[10001],cnt;

void getroot(int u,int f)

void dfs(int u,int d,int f)

int calc(int u,int d){

cnt=0; dfs(u,d,0); int l=1,r=cnt,sum=0;

sort(arr+1,arr+cnt+1);

for(;;++l){

while(r&&arr[l]+arr[r]>k) --r;

if(r【總結】

點分治是經典的分治思想在樹上的應用,是很重要的oi演算法。

其精髓在於把無根樹平均地分割成若干互不影響的子問題求解,極大降低了時間複雜度,是一種巧妙的暴力。

【注】

細心的讀者可能已經發現,**中在分治過程中,對下一層分治塊的總結點數處理可能會出錯,但是不影響複雜度,具體證明見

分治法學習

1.將問題的例項劃分為同乙個問題的幾個較小的例項,最好擁有同樣的規模 2.對這些較小的例項求解 一般使用遞迴方法,但在問題規模足夠小的時候,也會用其他方法 3.如果必要的話,合併這些較小問題的解,以得到原始問題的解。舉個例子說明下 比如計算n個數字的和。如果n 1,我們可以把該問題分解為它的兩個例項...

演算法學習 一 分治演算法

演算法學習 一 分治演算法 1.1 引言 當我們在處理一些問題時,由於這些問題要處理的資料很多,或者求解的過程很複雜,這時如果直接求解將會在時間上花費很長時間,或者根本沒辦法求出,對於這一類的問題,我們可以先把它分解為幾個子問題,找到並求出這些子問題的相應的解,然後再用適當的方法將他們組合成整個問題...

演算法學習 求割點

強連通分量 割點 去掉這個點之後,圖會被分成多個點集,點集之間的點無法相互到達 include include includeusing namespace std const int maxn 100010 struct note edge 2 maxn int st maxn top 0 voi...