由於蒟蒻太遜,現在才開始學動態點分治,寫乙個\(blog\)吧。
動態點分治是利用點分治的過程,建成一顆由子樹重心連線而成的點分樹,這棵樹的高度為\(\log n\)級別的,因此可以通過暴力跳父親完成修改操作。
建立點分樹的過程,就是按照點分治的流程,記錄上級重心並連線,即可獲得一棵點分樹。
點分樹的結構與原樹不相同,但是由於它具有優秀的樹高,同時可以發現,對於任意乙個節點,從該節點到點分樹根的路徑具有容斥的性質(可以把點分樹根節點當做包含所有點資訊的圓,那麼它的子節點就是乙個更小的圓,任何子節點的圓都包含在父節點內)。通過這一性質,我們可以通過從小範圍開始,逐步擴大範圍,同時容斥,就可以求出關於乙個節點的資訊了。
其他內容和細節,還是放到例題中去吧。
注:以下內容中,不經特別說明的樹均代表點分樹。
對於每個節點,維護其子樹中所有節點到達它的距離之和,可以利用兩個樹狀陣列,為了節約空間開銷,我們把每個節點的兩個樹狀陣列大小分別定為它的子樹的最大深度以及它的子樹到達其父親的最大距離(懶得計算也可以直接用子樹大小代替,因為其父節點一定與子樹中乙個節點相鄰,所以第二個樹狀陣列不用多開,這一點下面也會提到。不過不建議這樣做,畢竟空間開銷大了),這樣的空間複雜度上限為\(n \log n\)。
計算時,用父節點子樹全部滿足題意的值減去子節點子樹對父節點的貢獻(注:不滿足條件即貢獻為\(0\))。
對於乙個節點\(x\),記錄\(x\)子樹對\(x\)的貢獻為\(s1_x\),\(x\)子樹對\(fa_x\)的貢獻為\(s2_x\)。
雖然點分樹中的父子關係與原樹不同,但這樣的計算基於的基礎在於,每個節點的貢獻都會被算到且僅被算過一次。同時我們需要保證,計算貢獻時我們所用的任意點到詢問點距離為它們之間的最短距離。
假設我們需要求到達\(x_0\)節點距離不超過\(k\)的所有點的權值,設\(x_0\)到根節點的路徑為\(\\)。
先證明每個節點的貢獻都會被算到,且計算貢獻時我們所用的任意點到詢問點距離為它們之間的最短距離。
對於任意節點\(y\):
我們考慮建立點分樹的過程,對於每個節點,它都記錄了與其相鄰的原樹中的乙個連通塊所有節點的資訊,設節點\(u\)記錄的連通塊為\(t_u\)。
貢獻一定會被統計,因為\(t_=v\)(點集)。
如果\(y\)在\(t_\)中,那麼 在\(x_0\)中會計算\(y\)的貢獻,顯然此時計算的距離為\(x_0 \rightarrow y\)的最短路徑。
如果\(y\)不在\(t_\)中,由於\(t_ \subseteq t_ \subseteq \cdots \subseteq t_\),我們可以證明,包含\(y\)的最小連通塊\(t_\)會記錄\(x_0\)與\(y\)之間的最短距離。因為樹上最短距離只需要保證不走重複邊就可以了,那麼我們考慮\(x_i\)處,\(x_0\)與\(y\)一定分別屬於\(x_i\)的不同子樹,這裡的子樹指的不是原樹上的子樹,而是建立點分樹時的不同連通塊(根據建立點分樹的過程,如果\(x_0\)與\(y\)屬於\(x_i\)的同一子樹,那麼顯然包含\(y\)的最小連通塊不是\(t_\))。既然是不同子樹,那麼\(x_0\)到\(y\)的最短距離一定是\(x_0 \rightarrow x_i \rightarrow y\)(可以把\(x_i\)當成建點分樹時的\(lca\)來理解)。
所以,在\(x_i\)處,一定會計算\(y\)的貢獻。
證明完畢!
繼續,證明每個節點的貢獻僅被算過一次。
對於任意有貢獻的節點\(y\):
如果\(y\)離\(x_0\)的距離很近,那麼\(y\)重複走一些路徑,可能使得總距離仍然\(\le k\),但是這明顯是不符合題意的。
同樣設包含\(y\)的最小連通塊為\(t_\),那麼這次的貢獻會統計。
如果在乙個比\(t_\)大的集合\(t_\)中,\(y\)重複走一些路徑,使得總距離仍然\(\le k\),那麼\(y\)必然也出現在\(t_}\)中,根據這一性質,我們進行容斥,每次計算\(t_(k \ne 0)\)的答案時,把屬於\(t_}\)中的點在\(t_\)的貢獻去掉,就避免了重複統計。
最終\(y\)的貢獻只計算了一次。
證明完畢!
所以,動態點分治的計算方式是正確的。
細節:\(1.\)由於樹狀陣列不易維護距離為\(0\)的資訊(當然要強行維護也行),可以把我們要查詢的節點單獨加入答案。也就是\(x\)的權值不加入樹狀陣列\(s1_x\)中,但是必須加入\(s2_x\)中。
\(2.\)選擇\(st\)表求\(lca\)是本題的最佳選擇,因為它能夠\(o(1)\)計算\(lca\),能夠支撐大量兩點間距離資訊的訪問。
總結:點分樹利用了巧妙的結構,使得詢問節點到根節點的路徑必然形成大集合包含小集合的關係,從而不重不漏地計算了所有答案。同時,由於點分樹是特殊構造的樹,學習時必須完全區別開點分樹和原樹(與\(lct\)相似),才能避免概念混淆。
\(code:\)
#include#include#include#include#define n 100005
#define inf 1000000007
using namespace std;
int n,m,x,y,opt,lsz,mx1,mx2,val[n];
int tot,fr[n],nxt[n << 1],d[n << 1];
int rtsz,rt,sz[n],f[n],dep[n];
int ans=0,cnt,bg[n],lg2[n << 1],dfn[n << 1],st[n << 1][22];
bool vis[n];
struct bit
int getsum(int x)
return ans;
}void update(int x,int y)
}}s1[n],s2[n];
void add(int x,int y)
void dfs(int u,int f)
}int lca(int u,int v)
mx=max(mx,rn-sz[u]);
if (mx> 1]+1;
for (int j=1;j<=lg2[cnt];j++)
for (int i=1;i<=cnt-(1 << j)+1;i++)
if (dep[st[i][j-1]]st[i][j]=st[i][j-1]; else
st[i][j]=st[i+(1 << j-1)][j-1];
getrt(1,n);
lsz=n;
solve(rt);
while (m--)
return 0;
}
點分治 動態點分治
實在拖得太久了。先扔掉資料 分治的核心是盡量把乙個整體分成接近的兩個部分,這樣遞迴處理可以讓複雜度從n 變成nlogn。兩個問題,如何區分和如何算答案。對於第乙個問題,重心,然後就是找重心的方法,兩個dfs,對於第二個問題,對於每個重心算當前塊中每個點到重心的答案,然後由重心分開的塊要把多餘的資訊去...
點分治與動態點分治
點分治一般是用於解決樹上路徑問題。樹的重心 把重心這個點割掉後,使所形成的最大的聯通塊大小最小的點。可以證明重心子樹的大小最大不會超過 n over 2 重心可以通過 dfs 一遍求出。maxsiz x 表示割掉點x後所形成的的最大的聯通塊的大小 void dfs int x,int fa max ...
動態點分治
首先你要先會點分治,然後動態點分治就是把點分治可持久化一下,讓其不用再每次詢問時都重新做一遍 具體就是,你考慮做點分的時候,你在每個分治重心上統計它所管轄的所有點的資訊,並計算答案。那麼每個點的資訊只會出現在它上面的分治重心中,所以我們把每層分治重心向上一層的連邊,這樣每個點的資訊只會出現都在它所有...