Luogu P3806 模板 點分治1

2022-04-08 01:38:08 字數 3843 閱讀 4704

gate

我回來了...

本來是應該12月發的blog,沒想到拖到了現在,注意事項什麼的稍微有點忘了,以後再慢慢補充吧

點分治是一種樹上演算法。顧名思義,就是對每個點進行分治,計算它的子樹對答案的貢獻。

主要用於處理樹上路徑,且乙個點會被統計多次的問題。

以這道題為例:

詢問樹上距離為k的點對是否存在。

設當前的根節點為x。

若距離為k的點對存在,有兩種情況:經過x和不經過x。

經過時,若x的兩個相異的子樹中的兩個點到x的距離之和=k,

即x有子樹a,b,...,若有(x,ai)+(x,bj)=k,則x對答案做出了貢獻。

不經過時,則對x的每乙個子樹遞迴分治處理。

但是,在統計答案的過程中,難免會計算x的同一子樹中的情況,即(x,ai)+(x,aj)=k

ai到aj的距離應該在子樹a中計算,否則它們間的距離會被加上重複的路徑

比如上圖中統計1的答案會計算(1,4)+(1,5),其中路徑(1,2)被加了兩次。

實際上(4)到(5)的距離不是4而是2。

因此,計算完當前節點的答案後,還要減去它的子樹的重複答案。具體步驟在後面詳細說明。

可以發現,時間複雜度由x的子樹大小決定。

使x的最大子樹最小——x是樹的重心。

所以,遞迴處理每一棵子樹時,都要找到它的重心作為根節點。

綜上所述,點分治的步驟可以概括為:

(root:當前重心)

找root,處理root[

計算root的貢獻,遍歷root的子樹( 對於每棵子樹:

減去該子樹中的重複計算,

找該子樹root,處理root[...])]

在這裡一共用到了四個函式:

get_rt() —— 找root

divide() —— 分治處理

calc() —— 計算貢獻

dfs() —— 遞迴求深度,用來計算路徑長(根據每道題題意不同)。

vis[x]表示x是否被分治處理過。

因為分治的過程中可能會把樹旋轉成不同的形狀,

或者說,因為根變化了,已經統計完的點可能會在新的根節點下面。

這個變數只在divide()中更新,但是所有函式都要用到。

$get$_$rt()$

類似於樹形dp,是乙個遞迴找最大子樹的過程。

siz[x]表示以x為根的子樹大小(包括自己),

f[x]表示x的最大子樹大小。

sum表示要求root的這部分子樹的總點數,初始時為n。

某一點x的最大子樹,其實不一定在x的下面,也可能是x作為根時x的父親的那一部分子樹。

所以除了每次f[u] =max(f[u],siz[v])

,【注意這裡不要誤寫成f[v]】

還要在最後更新f[u] = max(f[u],sum-siz[u])

void get_rt(int u,int

fa)

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

if(f[u] <= f[rt]) rt =u;

}

$divide()$

首先將這個點標記vis,表示已經分治過了。

計算它的貢獻。

傳入$calc$的三個引數(u,nowdis,op)

分別表示當前點、到這個點為止已經走過的距離、是加入還是減去這個點的貢獻(加入為1,減去為-1)。

遍歷這個點的兒子時,首先判斷是否vis過;

減去這個子樹內部的重複計算:nowdis為邊(u,v)的權值,op為-1.

然後處理這棵子樹。

子樹的總點數sum即為siz[v](在上次get_rt中已經算好了),rt=0,f[0]=max(這裡可以是siz[v])

在這棵子樹中找到重心,並分治。

因為這次的操作是侷限在這個子樹中的,所以並不會影響到它的兄弟,並列操作即可。

void divide(int

u) }

$calc()$

前面已經提到了calc中的三個引數:(u,nowdis,op)

tot表示所要計算的子樹中的總點數,在dfs時計算。

dfs中傳入的引數分別為(u,fa,nowdis),含義相同

dfs完畢後,n^2列舉點對,計算它們的貢獻。

ans[x]表示距離為x的點對個數;

dis[i]表示從該子樹根節點到i的距離。

根據op的值不同,答案+1或-1。

void calc(int u,int nowdis,int

op)

$dfs()$

根據題意,在dfs中求出該子樹根節點到子樹中個點的距離。

利用tot,給子樹中的點重新編號1~tot,同時求出總點數。

到達某個點時,編號為++tot,且該點的dis即為nowdis.

然後再遍歷這個點的兒子節點並dfs;

同樣為了防止重複,要先判斷是否vis過。

新的nowdis即為原來的nowdis+w(u,v).

void dfs(int u,int fa,int

nowdis)

}

最後對於每個詢問k,檢查ans[k]是否大於0即可。

總結一下:

初始化:f[0] = sum = n,get_rt(1,0),divide(root);

$get$_$rt$(int u,int fa)  遞迴  需判斷(v == fa || vis[v])

$divide$(int u)  遞迴  更新vis[u],需判斷(vis[v])

$calc$(int u,int nowdis,int op)   無遞迴,無判斷

$dfs$(int u,int fa,int nowdis)  遞迴  需判斷(v == fa || vis[v])

完整**如下

#include#include

#include

#include

#define mogeko qwq

using

namespace

std;

const

int maxn = 20005

;int n,m,x,y,z,ans[10000005

];int

rt,sum,tot,siz[maxn],f[maxn],dis[maxn];

intcnt;

inthead[maxn],to[maxn],nxt[maxn],w[maxn];

bool

vis[maxn];

intread()

while('0'

<= ch && ch <= '9'

)

return x*f;

}void add(int x,int y,int

z) void dfs(int u,int fa,int

nowdis)

}void calc(int u,int nowdis,int

op)

void get_rt(int u,int

fa)

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

if(f[u] <= f[rt]) rt =u;

}void divide(int

u) }

intmain()

f[0] = sum =n;

get_rt(

1,0);

divide(rt);

for(int i = 1; i <= m; i++)

return0;

}

view code

luogu P3806 模板 點分治1

題目描述 給定一棵有n個點的樹 詢問樹上距離為k的點對是否存在。多次詢問 可離線 我們先隨意指定乙個虛擬根root,將這棵樹轉化成無根樹 樹上的路徑可以分為兩類,1.經過根節點u的路徑 2.完全在u子樹裡 不經過u 的 對於1,用dis表示當前結點到根節點root的路徑長度,則root的子樹中兩個節...

Luogu P3806 模板 點分治1

給定一棵有 n nn 個點,邊權的樹,回答 m mm 個詢問,每次詢問樹上距離為 k kk 的點對是否存在。資料範圍n 1 04,m 100,邊權 10000,k 107 n leqslant 10 4,m leqslant 100,texttt leqslant 10000,k leqslant ...

luogu P3806 模板 點分治1

給你一棵樹,路徑有長度,多次詢問,每次給出 k,問你是否存在路徑長度為 k 的點對。這道題我們用分治的方法。那我們假設要解決乙個樹,那我們先選重心作為根節點 為了減少高度節省時間 然後就分成兩種討論,一種是路徑都在同乙個子樹中,那這個我們就可以把問題轉換為解決這個子樹。另一種就是在兩個不同的子樹中 ...