點分治學習筆記

2022-05-06 22:06:15 字數 3376 閱讀 7807

關於點分治,其實思想是非常好理解的,模擬在數列上或是在平面上的分治演算法(如歸併排序,平面最近點對等),我們可以從字面上理解該演算法:

以乙個點為界限,將一棵樹分成若干個子樹,當劃分到一定規模,就對每個子樹分別進行求解

感性理解就好了

感受乙個演算法最直觀的辦法,就是來看一道模板題。

給定一棵有\(n\)個點的樹,詢問樹上長度為\(k\)的鏈是否存在。

首先可以很直觀的知道,對於樹上的任意乙個點,有很多條經過它的鏈

那麼,對於本題,我們是否可以在能夠接受的時間內對這些經過該點的鏈進行求解呢?

答案是肯定的,只需要以該節點為根節點,對整顆樹進行一遍\(\text\),求出各個點到該點的距離,然後就可以用桶排等方法解決該問題。

那麼對於剩下的沒有被處理到的鏈呢?

自然,我們可以以這個點,將整棵樹斷掉,將它的子樹分開遞迴分治求解,這樣這道題目就解決啦!

咳咳,真的這麼簡單嗎?

我們來看一張圖

多麼優雅的一條鏈!

如果我們一開始以\(1\)為根節點,按照這個思路,我們需要進行\(n\)次操作,這樣肯定是不行的。

也就是說,我們需要找到乙個節點,使得在將其斷掉之後,剩下的各個子樹的大小相對均勻,這樣在進行分治求解的時候就可以讓時間複雜度最優。

所以這裡需要引入乙個新的概念:

那麼如何求樹的重心呢?

我們可以採取一種類似於\(dp\)的演算法,因為我們要使最大的子樹節點數最少,於是我們可以任選乙個點進行\(dfs\),在搜尋過程中,記錄每乙個點的最大的子樹大小,然後進行操作,即

\[dp[u]=max(siz[son[u]],sum-siz[u])

\]\(sum\)表示這顆子樹一共有多少個節點,\(siz[i]\)即子樹大小

這樣的話,我們就只需要在該子樹中找到最小的\(dp[i]\),這樣\(i\)就是我們要找的重心了。

是不是很簡單?

貼一小段**

//root預設為0,dp[0]=inf

void get_root(int u,int fa,int sum)

dp[u]=max(dp[u],sum-siz[u]);

if(dp[u]那麼,如何統計答案呢?

對於本題,提供\(3\)種方法供君選擇

說明:\(dis[u]\)表示\(u\)節點到重心的距離,\(siz[u]\)表示以\(u\)為根的子樹大小,\(root\)表示當前子樹重心

\(1.\)暴力列舉法

我們將所有的節點到重心的距離\(dis[u]\)通過一遍\(dfs\)記錄下來,然後開乙個桶,兩兩組合,統計答案。這樣的話,會有乙個問題,就是在同一條路徑上的節點的答案也會被統計,比如\(dis[u]+dis[son[u]]=k\),但是這兩個節點並沒有到重心的一條鏈,所以需要刪去。

那麼如何做呢?

簡單容斥一下就好了

\[ans=ans(以重心為根的子樹)-\sum ans(以重心的孩子為根的子樹)

\]時間複雜度為單次\(o(siz[root]^2)\),且有一定侷限性——\(k\)太大時無法使用

$ 2.$配對法

這是乙個在本題跑得飛起的計算方法

假設一共有\(son_1,son_2,son_3,...,son_n\)這些多棵子樹

令\(vis[j]\)陣列表示在求解到第\(i\)棵子樹的答案時,前\(i-1\)棵子樹是否存在到重心長度為\(j\)的路徑

這樣一來,我們就只需要在每棵子樹當中對於每乙個詢問,列舉找到可以湊成答案的路徑即可

時間複雜度為單次\(o(m*siz[root])\),由於詢問較少,跑的飛起

但注意,在還原陣列的時候,需要將

同樣,也有一定的侷限性——\(k\)太大時同樣無法使用

\(3.\)two pointers

維護\(l,r\)兩個指標,將所有得到的\(dis[i]\)從小到大排序,這樣的話,就可以保證\(dis\)陣列單調遞增,有兩個思路供君選擇:

\(1)\)直接標記(僅針對本題)

在\(dfs\)求解\(dis[i]\)時,可以記錄每乙個節點對應來自哪一棵子樹,記為\(tag[i]\)然後可以按照這樣的思路:

令\(l=0,r=siz[root]\)

如果當前點已有答案,跳過

如果\(dis[l]+dis[r]>k\),就\(--r\),這樣才有可能有解

如果\(dis[l]+dis[r],就\(++l\),同理

如果\(dis[l]+dis[r]=k \quad且\quad tag[l]==tag[r]\),就看\(dis[r-1]\)的大小,並進行相應調整

如果上述條件都不滿足,則對於這個\(k\)有解

\(2)\)字首統計

我們可以化等為不等,記錄\(\le k\)和\(\le k-1\)的路徑條數

同樣令\(l=0,r=siz[root]\)

若\(dis[l]+dis[r]<=k\),則說明在\([l+1,r]\)的\(dis\)都可以組成答案,此時\(++l\);

否則\(--r\);

這種方法同樣需要容斥。

兩種方法的時間複雜度均為單次\(o(m*siz[root])\),且不受\(k\)的限制,同時這種思想也在非常多的題目上有所運用,如\(noi2019day1t3\)

大體思路就是這樣,共\(3\)步:

\(1.\)找樹的重心

\(2.\)求解經過重心的鏈對答案的貢獻

\(3.\)在各個子樹內求解

於是這個題目就完結辣owo~

貼**(上面講的很清楚了於是沒有注釋qwq)

#includeusing namespace std;

const int maxn=1e5+10;

int n,m;

struct cce[maxn<<2];

int head[maxn],cnt;

int siz[maxn],dp[maxn],vis[maxn],q[maxn],ans[maxn];

void add(int x,int y,int z)

int root=0;

void get_root(int u,int fa,int sum)

dp[u]=max(dp[u],sum-siz[u]);

if(dp[u]

}int dep[maxn],dis[maxn],tot;

void get_dis(int u,int fa)

}void get_ans(int u,int now,int val)

}void solve(int u)

}int main()

點分治學習筆記

點分治主要用來處理樹上路徑問題,可以統計樹上點到點的所有路徑,複雜度o nlogn 基於樹上的結點進行分治,不斷將一棵樹拆成多顆子樹處理 選擇點時為了防止退化成鏈的情況,如果選點後左右子樹越大,遞迴層數越多,時間越慢,反之則越快,我們每次選擇子樹內的重心 void getroot int u,int...

點分治學習筆記

point divide and rule 澱粉質就是在樹上,依靠不停的遞迴和分治,解決相同的子問題 先來看看模板題 tree 就是找樹上 k 的路徑有多少 我們可以分兩種情況討論 1.經過根節點 p 的路徑 2.不經過根節點 p 的路徑 第二種情況可以通過遞迴來處理,我們直接來討論第一種情況 設當...

點分治學習筆記

銀月城傳送門 集訓day9上午各種神仙分治真心聽不懂,自己頹著把點分治學了,簡單 並不 寫一下再加深一下理解吧。一 點分治是個啥 點分治是通常用來處理樹上路徑統計問題的一種分治方法。尤其適用於大規模的資料處理。時間複雜度最高為o nlogn 例如求樹上兩點間距離,點分治就遠優於dfs求兩點距離 o ...