【學習筆記】樹論—點分樹(動態點分治)
【前言】
氡態淀粉質 / 墊糞鼠
點分治是一種樹上分治算法,常用以處理樹上路徑相關信息的統計。在點分治的基礎上加以變化,構造一顆支持快速修改的重構樹,就成了點分樹。
雖說名字里帶個動態,但也有人認為它應該算作靜態數據結構。
(據教練所說,點分樹是近幾年的新興熱門考點...於是就有了這篇總結...)
一:【算法理解及復雜度分析】
前置芝士:需要有良好的 點分治 基礎。
點分治的核心思想在於依據重心划分子連通塊,其良好的性質保證了最多只會分治 \(\log n\) 層。有了這一特性,便可使用各種暴力計算答案。
那么我們按照分治遞歸的順序提一顆新樹出來,易知樹高是 \(O(\log n)\) 的。
具體地說,對於每一個找到的重心,將上一層分治時的重心設為它的父親,得到一顆大小不變、最多 \(\log n\) 層的虛樹(或者理解為重構樹。亦可稱點分樹,意義一樣)。
在這顆虛樹上,奇妙的性質產生了:虛樹上所有點的子樹大小之和為 \(O(n\log n)\) 。
證明很簡單:每個點會被從根到它的路徑上最多 \(\log n\) 個祖先所統計,因此總復雜度為 \(\sum_{i=1}^{n}depth(i)=O(n\log n)\) 。
如果要對某個點進行修改操作,直接在虛樹上暴力跳父親,改變 \(O(\log n)\) 個祖先的虛子樹信息即可。
Q: 虛子樹的信息與原樹有啥關系呢?
A: 點 \(x\) 在虛樹上的子樹集合就是原樹中以 \(x\) 為重心(分治中心)時所囊括到的連通塊。
如下圖(黑邊為原樹,灰邊為虛樹,藍色編號為分治的順序):
以 \(1\) 為重心做分治時,所囊括的連通塊為 \(\{1,2,3,4,5,6,7,8,9,10\}\),刪去該點后被划分為了 \(\{2,3,4,5,6\},\{7,8,9,10,11\}\) 兩個子連通塊;
以 \(2\) 為重心做分治時,所囊括的連通塊為 \(\{2,3,4,5,6\}\),刪去該點后被划分為了 \(\{3,4\},\{5\},\{6\}\) 三個子連通塊;
以 \(7\) 為重心做分治時,所囊括的連通塊為 \(\{7,8,9,10,11\}\),刪去該點后被划分為了 \(\{8,9\},\{10,11\}\) 兩個子連通塊;
......
那么能用它求什么呢?
比如我們要統計某個點 \(x\) 到其他所有點的距離之和,即 \(\sum_{y=1}^{n}dis(x,y)\) 。對於任意一個 \(y\),首先在虛樹上找到它與 \(x\) 的 \(lca\)(或者說囊括連通塊同時包含 \(x,y\) 的所有虛樹節點中深度最深的那一個),易知在以此點為重心划分子連通塊時 \(x,y\) 會首次被分割開來,因此該點必定在原樹的 \(x,y\) 路徑上。
所以我們只需要在這些 \(lca\) 的虛子樹中尋找 \(y\) 即可,此時記錄虛子樹信息的作用便顯現出來了。
而對於一個 \(x\),可能的 \(lca\) 最多存在 \(\log n\) 個,因此通常使用暴力枚舉+簡單容斥的方法來統計 \(y\) 的貢獻。具體見下文 【 計算貢獻】。
二:【算法實現】
【模板】點分樹 / 震波 \(\text{[P6329]}\) \(\text{[Bzoj3730]}\)
【題目大意】 維護一顆帶點權樹,需要支持兩種操作:修改 \(x\) 的點權,查詢與點 \(x\) 距離不超過 \(K\) 的點權值之和。
1.【建點分樹】
找到第一個重心 \(rt\) 后,先遍歷整顆樹得到 \(rt\) 的子樹信息,然后刪去 \(rt\),得到若干個連通塊(\(rt\) 的不同子樹)。分別找到這些子樹里的重心(在虛樹上使其成為 \(rt\) 的兒子),然后遞歸處理這些子連通塊。
實際上大體和淀粉質是相同的,只是將淀粉質中“計算答案”的操作改為了“統計虛子樹信息”。
2.【計算貢獻】
以下用 \(fa_i\) 表示點 \(i\) 在虛樹上的父親,\(subtree(i)\) 為點 \(i\) 在虛樹上的子樹集合,\(fatree(i)\) 為點 \(i\) 在虛樹上的祖先集合,\(dis(i,j)\) 為 \(i,j\) 兩點在原樹上的距離,\(A_i\) 為點 \(i\) 的點權。
設:
\(f_1(i,j)=\sum_{x\in subtree(i),dis(x,i)\leqslant j}A_x\),即虛樹上 \(i\) 的子樹中與 \(i\) 距離小於等於 \(j\) 的點權值之和;
為了除去某一個虛兒子子樹的貢獻,還需要:
\(f_2(i,j)=\sum_{x\in subtree(i),dis(x,fa_i)\leqslant j}A_x\),即虛樹上 \(i\) 的子樹中與 \(fa_i\) 距離小於等於 \(j\) 的點權值之和。
在一次查詢 \((x,k)\) 中,對於虛樹上的一對父子節點 \((i,fa_i)\),\(subtree(fa_i)-subtree(i)\) 對答案的貢獻為 \(G(i,fa_i)=f_1(fa_i,k-dis(x,fa_i))\) \(-\) \(f_2(i,k-dis(x,fa_i))\) 。
則有 \(ans(x,k)=f_1(x,k)+\) \(\sum_{i\in fatree(x),fa_i\neq 0} G(i,fa_i)\),注意前面那個 \(f_1(x,k)\) 是因為容斥求和后 \(subtree(x)\) 沒有被統計進去,所以要單獨拿出來算。
修改點權時同理,把所有對於 \(f_1,f_2\) 的查詢操作換成修改就可以了。
3.【維護信息】
計算 \(dis\) 可以用樹剖求 \(\text{LCA}\) 在線查詢。
也可以用歐拉序 + \(\text{ST}\) 表。或者用在每次找完根后,\(dfs\) 遍歷一下子樹預處理 \(dis\) 數組。兩者都是花費 \(O(n\log n)\) 的時間省下了 \(O(q\log^2 n)\) 的時間,但經實際測試,\(\text{ST}\) 表被樹剖吊起來錘,預處理與樹剖不相上下,emm...過於柯怕
維護 \(f_1,f_2\) 可以對每個節點開一棵動態開點線段樹,下標為 \(dis\)(即 \(f_1,f_2\) 的第二維)。時間復雜度為 \(O(n\log^2 n)\),空間按照 \(dis\) 范圍的上界壓縮一下可以做到 \(O(n\log n)\)(如果偷懶統一使用同一個上界 \([0,n]\) 可能會達到 \(O(n\log^2 n)\))。但線段樹常數略大,可能會被卡(雖然我過了),換成樹狀數組會穩一點。
Q: 樹狀數組咋動態開點啊?
A: 當然是用動態分配內存的 \(vector\) 啊!
4.【實現細節】
-
子連通塊大小不要直接用 \(\text{size(to)}\)(這都是從淀粉質那里遺留下來的問題了,但依舊有人不重視)
-
一般不需要把虛樹的實際形態建出來,但如果能用到的話,注意不要和原樹搞混。
-
樹狀數組大小要設置得當。設 \(now=|subtree(i)|\),由於 \(i\) 在 \(subtree(i)\) 中為重心,所以 \(f_1(i,j)\) 中 \(j\) 的值域為 \([0\sim \frac{now}{2}]\),\(f_2(i,j)\) 中 \(j\) 的值域為 \([1\sim now]\) 。由於下標可以為 \(0\),在樹狀數組內部還需要統一向后移一位。也就是說 \(f_1,f_2\) 的大小要分別設置為 \(\frac{now}{2}+1\) 和 \(now+1\) 。
-
如果預處理了每個點在虛樹上的祖先,修改 / 查詢 中跳父親時需要倒序枚舉(具體見代碼)。
-
關於卡常:點分樹做題經常會用到 \(\text{vector},\) \(\text{set},\) \(\text{multiset},\) \(\text{priority queue}\) 之類的東西,如果您嫌棄它們太慢且不願開 \(\text{O2}\),自備幾個能代替 \(\text{STL}\) 的模板吧....
5.【Code】
(線段樹的代碼就不放了)
【在線查詢 Dis】
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#define LL long long
#define Re register int
using namespace std;
const int N=1e5+3,inf=2e9,logN=17;
int n,m,o,x,y,T,op,lastans,A[N],deep[N],head[N];
struct QAQ{int to,next;}a[N<<1];
inline void add(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
struct LCA{
int fa[N],top[N],son[N],size[N];
inline void dfs1(Re x,Re Fa){
size[x]=1,deep[x]=deep[fa[x]=Fa]+1;
for(Re i=head[x],to;i;i=a[i].next)
if((to=a[i].to)!=Fa){
dfs1(to,x),size[x]+=size[to];
if(size[son[x]]<size[to])son[x]=to;
}
}
inline void dfs2(Re x,Re rt){
top[x]=rt;
if(!son[x])return;
dfs2(son[x],rt);
for(Re i=head[x],to;i;i=a[i].next)
if((to=a[i].to)!=fa[x]&&to!=son[x])dfs2(to,to);
}
inline void build(){dfs1(1,0),dfs2(1,1);}
inline int lca(Re x,Re y){
while(top[x]!=top[y]){
if(deep[top[x]]<deep[top[y]])swap(x,y);
x=fa[top[x]];
}
if(deep[x]>deep[y])swap(x,y);
return x;
}
}T1;
inline int Dis(Re x,Re y){return deep[x]+deep[y]-(deep[T1.lca(x,y)]<<1);}//查詢原樹中點x,y的距離
struct BIT{
int n;vector<int>C;
inline void build(Re N){C.resize((n=N)+1);}//使用到的上限為n,空間開n+1
inline void add(Re x,Re v){++x;while(x<=n)C[x]+=v,x+=x&-x;}//由於dis可能為0,所以在BIT里面統計向后移一位,查詢同理
inline int ask(Re x){++x,x=min(x,n);Re ans=0;while(x)ans+=C[x],x-=x&-x;return ans;}//注意要和維護的上界取最小值,防止越界
}TR1[N],TR2[N];
int rt,sum,fa[N],vis[N],maxp[N],size[N];
inline void getrt(Re x,Re fa){//獲取該連通塊的重心
size[x]=1,maxp[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(!vis[to=a[i].to]&&to!=fa)
getrt(to,x),size[x]+=size[to],maxp[x]=max(maxp[x],size[to]);
maxp[x]=max(maxp[x],sum-size[x]);
if(maxp[x]<maxp[rt])rt=x;
}
inline void sakura(Re x,Re Fa){//處理重心x所囊括的連通塊
Re now=sum;vis[x]=1,fa[x]=Fa;
TR1[x].build(now/2+1),TR2[x].build(now+1);//由重心性質可知,TR1會使用[0,now/2],TR2會使用[1,now],向后移一位變為[1,now/2+1]和[2,now+1]
for(Re i=head[x],to;i;i=a[i].next)
if(!vis[to=a[i].to])
sum=size[to]>size[x]?now-size[x]:size[to],maxp[rt=0]=inf,getrt(to,0),sakura(rt,x);//注意子連通塊大小不要直接用size[to]
}
inline void change(Re x,Re v){
TR1[x].add(0,v);//subtree(x)
for(Re i=x;fa[i];i=fa[i]){//在虛樹上面跳父親
Re tmp=Dis(x,fa[i]);
TR1[fa[i]].add(tmp,v);
TR2[i].add(tmp,v);
}
}
inline int ask(Re x,Re K){
Re ans=TR1[x].ask(K);//subtree(x)
for(Re i=x;fa[i];i=fa[i]){//在虛樹上面跳父親
Re tmp=Dis(x,fa[i]);if(tmp>K)continue;
ans+=TR1[fa[i]].ask(K-tmp);
ans-=TR2[i].ask(K-tmp);
}
return ans;
}
int main(){
// freopen("123.txt","r",stdin);
in(n),in(T),m=n-1;
for(Re i=1;i<=n;++i)in(A[i]);
while(m--)in(x),in(y),add(x,y),add(y,x);
T1.build(),sum=n,maxp[rt=0]=inf,getrt(1,0),sakura(rt,0);
for(Re i=1;i<=n;++i)change(i,A[i]);
while(T--){
in(op),in(x),in(y),x^=lastans,y^=lastans;
if(op)change(x,y-A[x]),A[x]=y;
else printf("%d\n",lastans=ask(x,y));
}
}
【預處理 Dis】
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#define LL long long
#define Re register int
using namespace std;
const int N=1e5+3,inf=2e9,logN=17;
int n,m,o,x,y,T,op,lastans,A[N],head[N];
struct QAQ{int to,next;}a[N<<1];
inline void add(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
struct BIT{
int n;vector<int>C;
inline void build(Re N){C.resize((n=N)+1);}//使用到的上限為n,空間開n+1
inline void add(Re x,Re v){++x;while(x<=n)C[x]+=v,x+=x&-x;}//由於dis可能為0,所以在BIT里面統計向后移一位,查詢同理
inline int ask(Re x){++x,x=min(x,n);Re ans=0;while(x)ans+=C[x],x-=x&-x;return ans;}//注意要和維護的上界取最小值,防止越界
}TR1[N],TR2[N];
int rt,sum,gs[N],vis[N],maxp[N],size[N],frt[N][20],fdis[N][20];
inline void getrt(Re x,Re fa){//獲取該連通塊的重心
size[x]=1,maxp[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(!vis[to=a[i].to]&&to!=fa)
getrt(to,x),size[x]+=size[to],maxp[x]=max(maxp[x],size[to]);
maxp[x]=max(maxp[x],sum-size[x]);
if(maxp[x]<maxp[rt])rt=x;
}
inline void getdis(Re x,Re rt,Re fa,Re d){//遍歷該連通塊預處理dis
frt[x][++gs[x]]=rt,fdis[x][gs[x]]=d;//順手把祖先也存下來,后面一起訪問
for(Re i=head[x],to;i;i=a[i].next)
if(!vis[to=a[i].to]&&to!=fa)getdis(to,rt,x,d+1);
}
inline void sakura(Re x){//處理重心x所囊括的連通塊
Re now=sum;vis[x]=1,getdis(x,x,0,0);
TR1[x].build(now/2+1),TR2[x].build(now+1);//由重心性質可知,TR1會使用[0,now/2],TR2會使用[1,now],向后移一位變為[1,now/2+1]和[2,now+1]
for(Re i=head[x],to;i;i=a[i].next)
if(!vis[to=a[i].to])
sum=size[to]>size[x]?now-size[x]:size[to],maxp[rt=0]=inf,getrt(to,0),sakura(rt);//注意子連通塊大小不要直接用size[to]
}
inline void change(Re x,Re v){
TR1[x].add(0,v);//subtree(x)
for(Re i=gs[x];i>=2;--i){//注意要倒序枚舉
Re tmp=fdis[x][i-1];
TR1[frt[x][i-1]].add(tmp,v);
TR2[frt[x][i]].add(tmp,v);
}
}
inline int ask(Re x,Re K){
Re ans=TR1[x].ask(K);//subtree(x)
for(Re i=gs[x];i>=2;--i){//注意要倒序枚舉
Re tmp=fdis[x][i-1];if(tmp>K)continue;
ans+=TR1[frt[x][i-1]].ask(K-tmp);
ans-=TR2[frt[x][i]].ask(K-tmp);
}
return ans;
}
int main(){
// freopen("123.txt","r",stdin);
in(n),in(T),m=n-1;
for(Re i=1;i<=n;++i)in(A[i]);
while(m--)in(x),in(y),add(x,y),add(y,x);
sum=n,maxp[rt=0]=inf,getrt(1,0),sakura(rt);
for(Re i=1;i<=n;++i)change(i,A[i]);
while(T--){
in(op),in(x),in(y),x^=lastans,y^=lastans;
if(op)change(x,y-A[x]),A[x]=y;
else printf("%d\n",lastans=ask(x,y));
}
}
三:【常見套路、經典例題】
(注: 后面的題都不再用文字具體闡述 \(f_1,f_2\) 含義,且只放核心代碼)
1.【另外兩道板題】
-
爍爍的游戲 \(\text{[Bzoj4372]}\) (和模板題震波類似,用一個數據結構進行維護)
-
\(\text{Atm}\) 的樹 \(\text{[Bzoj4317] [Bzoj2051] [Bzoj2117]}\)(二分答案后又是一只震波)
2.【開店】
開店 \(\text{[HNOI2015] [P3241]}\)
【題目大意】 維護一顆帶點權、邊權樹,每次給出 \(x,l,r\),查詢 \(\sum_{l\leqslant A_y \leqslant r}dis(x,y)\),其中 \(A_y\) 為 \(y\) 的點權。
說起這道題,想起一張圖(來自 \(\text{hychyc}\) 巨佬的主席樹做法)

(窩還是老老實實打點分樹吧...)

(1).【分析】
統計貢獻還是和模板題一樣的套路,設:
\(f_1(i,j)=\sum_{x\in subtree(i),A_x\leqslant j}dis(x,i)\)
\(f_2(i,j)=\sum_{x\in subtree(i),A_x\leqslant j}dis(x,fa_i)\)
在查詢點 \(x\) 的答案時,上面只算了合法點到祖先的距離,漏掉了 \(x\) 到 \(fa_i\) 這一段,所以還要記點數:
\(g_1(i,j)=\sum_{x\in subtree(i),A_x\leqslant j}1\)(這里就不需要設 \(g_2\) 了,直接相減即可)。
查詢時差分一下,只算權值小於等於 \(k\) 的距離之和。
則有 \(ans(x,k)=f_1(x,k)+\) \(\sum_{i\in fatree(x),fa_i\neq 0} G(i,fa_i)\)
其中 \(G(i,fa_i)=\) \(f_1(fa_i,k)-f_2(i,k)\) \(+dis(x,fa_i)\times (g_1(fa_i,k)-g_1(i,k))\) 。
由於這題沒有修改操作,所以直接在建虛樹時開個 \(\text{vector}\) 預排序,順便求出前綴和,查詢時二分位置即可。
另外,為減小常數可以把柿子拆開,在一次 \(k\) 查詢中對於虛樹上每個祖先只使用一次二分。
空間復雜度:\(O(n\log n)\) 。
時間復雜度:\(O((n+q)\log^2 n)\) 。
(2).【Code】
struct QWQ{
int v;LL S1,S2;QWQ(Re V=0,LL s1=0,LL s2=0){v=V,S1=s1,S2=s2;}
inline bool operator<(const QWQ &O)const{return v<O.v;}
};
vector<QWQ>TR[N];
inline void getdis(Re x,Re fa,Re rt,Re d){
TR[rt].push_back(QWQ(A[x],d,Fa[rt]?Dis(x,Fa[rt]):0));//如果rt沒有父親就不要求dis了
for(Re i=head[x],to;i;i=a[i].next)if(!vis[to=a[i].to]&&to!=fa)getdis(to,x,rt,d+a[i].w);
}
inline void sakura(Re x){
Re now=sum;vis[x]=1,getdis(x,0,x,0);
sort(TR[x].begin(),TR[x].end());//預排序
for(Re i=1;i<now;++i)TR[x][i].S1+=TR[x][i-1].S1,TR[x][i].S2+=TR[x][i-1].S2;//前綴和
for(Re i=head[x],to;i;i=a[i].next)if(!vis[to=a[i].to])
sum=size[to]>size[x]?now-size[x]:size[to],maxp[rt=0]=inf,getrt(to,0),Fa[rt]=x,sakura(rt);
}
inline QWQ get(Re x,Re K){//獲取權值小於等於K的點信息之和
vector<QWQ>::iterator it=upper_bound(TR[x].begin(),TR[x].end(),QWQ(K));
if(it==TR[x].begin())return QWQ();--it;return QWQ((it-TR[x].begin())+1,it->S1,it->S2);
}
inline LL ask(Re x,Re K){//查詢權值小於等於K的點與x的距離之和
QWQ TR=get(x,K);LL ans=TR.S1;
if(Fa[x])ans-=TR.S2+(LL)Dis(x,Fa[x])*TR.v;//為迎合拆柿子,這里把第一次計算拿出來了
for(Re i=Fa[x];i;i=Fa[i]){
TR=get(i,K),ans+=TR.S1+(LL)Dis(x,i)*TR.v;
if(Fa[i])ans-=TR.S2+(LL)Dis(x,Fa[i])*TR.v;
}
return ans;
}
3.【幻想鄉戰略游戲】
幻想鄉戰略游戲 \(\text{[ZJOI2015] [P3345]}\)
【題目大意】 維護一顆帶點權、邊權樹(樹上點的度數不超過 \(20\))。現有若干次修改點權的操作,每次操作結束后您需要選出一個核心點 \(x\) 使得 \(F(x)\) 最小,其中 \(F(x)=\sum_{i=1}^{n}dis(x,i)\times A_i\),\(A_i\) 為 \(i\) 的點權。
(1).【分析】
這題關鍵在於分析性質猜結論。
假設當前核心為 \(x\),將 \(x\) 設為樹的根,定義 \(S_A(i)\) 為 \(i\) 子樹內所有點的點權之和。
對於 \(x\) 的任意一個兒子節點 \(y\),若將核心改為 \(y\),那么 \(\Delta F_{x \to y} =F(y)-F(x)=(S_A(x)-2S_A(y))\times dis(x,y)\) 。選 \(y\) 更優當且僅當滿足 \(\Delta F_{x \to y}<0\) 即 \(S_A(x)<2S_A(y)\),易知對於一個 \(x\) 滿足該式的 \(y\) 最多只存在一個。
於是一個經過優化的暴力就產生了:每次詢問從根開始,不斷進入比它更優的兒子,如果找不到更優則說明本身已是最優。
但很多人只講到這些就開始扯點分樹信息維護了,實際上是不嚴謹的,我們還需要證明:“在以 \(x\) 為根時,若 \(y\) 沒有 \(x\) 優秀(表現為 \(\Delta F_{x \to y}>0\)),則 \(y\) 子樹里的點也一定沒有 \(x\) 優秀”。
設 \(y\) 的兒子為 \(y'\),考慮還是在以 \(x\) 為根的前提下記算貢獻:
\(\Delta F_{y \to y'}\) \(=(S_A(y)-2S_A(y')+S_A(x)-S_A(y))\times dis(y,y')\) \(=(S_A(x)-2S_A(y'))\times dis(y,y')\)
又因為 \(S_A(y)\geqslant S_A(y')\)(這題 \(A_i\) 應該是不為負的,所以能得出這個關系式)
若 \(\Delta F_{x \to y}>0\) 則 \(S_A(x)>2S_A(y)\geqslant 2S_A(y')\)
因此 \(\Delta F_{y \to y'}>0,\) \(\Delta F_{x \to y'}=\Delta F_{x \to y}+\Delta F_{y \to y'}>0\) 。
證畢。
暴力單次詢問復雜度 \(O(20depth)\),而點分樹有着 \(depth=O(\log n)\) 的天然優勢,我們可以把跳兒子的過程轉移到點分樹上進行。具體的說,從第一個重心開始,枚舉它在原樹上的兒子,若找到了比他更優的點,則跳到該子樹(或者說子連通塊)的重心。容易證明這樣做一定不會錯過最優解。
現在的問題只剩下快速計算 \(F(x)\) 了,按照套路,設:
\(f_1(i)=\sum_{x\in subtree(i)}dis(x,i)\times A_x\)
\(f_2(i)=\sum_{x\in subtree(i)}dis(x,fa_i)\times A_x\)
\(g_1(i)=\sum_{x\in subtree(i)}A_x\)
則有 \(F(x)=f_1(x)+\) \(\sum_{i\in fatree(x),fa_i\neq 0} G(i,fa_i)\)
其中 \(G(i,fa_i)=\) \(f_1(fa_i)-f_2(i)\) \(+dis(x,fa_i)\times (g_1(fa_i)-g_1(i))\) 。
由於這題的 \(f_1,f_2,g_1\) 都只有一維,直接用數組存下來就好了。
空間復雜度:\(O(n\log n)\) 。
時間復雜度:\(O(n\log n+20q\log^2n)\) 。
(這題不知為啥常數小得驚人,不吸氧最慢的點只跑了 \(900ms\))
(2).【Code】
inline void change(Re x,Re v){//Sv即為g1
TR1[x]+=v*0,Sv[x]+=v;
for(Re i=gs[x];i>=2;--i){
Re tmp=fdis[x][i-1];
TR1[frt[x][i-1]]+=(LL)v*tmp;
TR2[frt[x][i]]+=(LL)v*tmp;
Sv[frt[x][i-1]]+=v;
}
}
inline LL ask(Re x){//計算F(x)
LL ans=TR1[x];
for(Re i=gs[x];i>=2;--i){
Re tmp=fdis[x][i-1];
ans+=TR1[frt[x][i-1]];
ans-=TR2[frt[x][i]];
ans+=(LL)(Sv[frt[x][i-1]]-Sv[frt[x][i]])*tmp;
}
return ans;
}
inline LL find(Re x){
LL tmp=ask(x);
for(Re i=T0.head[x];i;i=T0.a[i].next)//這里枚舉原樹
if(ask(T0.a[i].to)<tmp)return find(T0.a[i].rt);//如果to比x優秀就進入to所在連通塊的重心(x在虛樹上的兒子)
return tmp;
}
4.【小清新數據結構題】
【題目大意】 維護一顆帶點權樹,需要支持兩種操作:修改 \(x\) 的點權,查詢以點 \(x\) 為根時的 \(\sum_{i=1}^{n}(\sum_{j\in sub(i)}A_j)^2\),其中\(A_j\) 為 \(j\) 的點權,\(sub(i)\) 為點 \(i\) 子樹內的節點集合。
(1).【分析】
一看就知道是個喪心病狂拆柿子題。
上公式(以 \(x\) 為根):
設 \(sum=\sum_{i=1}^{n}A_i\),\(S(i)=\sum_{j\in sub(i)}A_j\)
\(\sum_{i=1}^{n}S_i\) \(=\sum_{i=1}^{n}A_i(dis(i,x)+1)\) \(=sum+\sum_{i=1}^{n}dis(i,x)\times A_i\)(每個點會在自己以及它的 \(dis\) 個祖先處被統計到)
設 \(F=\sum_{i=1}^{n}dis(i,x)\times A_i\)(用與 幻想鄉戰略游戲 同樣的方法可以求得)
由於 \(\sum_{i=1}^{n}S_i(sum-S_i)\) 始終為一個定值(對於每條邊 \(x,y\),兩邊的連通塊點權之和乘起來然后再求和),我們可以先 \(O(n)\) 預處理出來,設為 \(tmp\) 。
則 \(ans(x)=\sum_{i=1}^{n}S_i^2\) \(=sum\sum_{i=1}^{n}S_i-tmp\) \(=sum(sum+F)-tmp\) 。
空間復雜度:\(O(n\log n)\) 。
時間復雜度:\(O((n+q)\log n)\) 。
(2).【Code】
代碼就不放了,畢竟和上一道差不多,只是多了個 \(dfs\) 預處理 \(tmp\)。

5.【成都七中】
成都七中 \(\text{[Ynoi2011] [P5311]}\)
【題目大意】 由一顆樹,樹上每個節點有一種顏色,每次查詢給出 \(l,r,x\),求保留樹上編號在 \([l,r]\) 內的點,\(x\) 所在聯通塊中顏色種類數。
(1).【分析】
這題比較難想。
先建出點分樹,對於一次查詢 \((l,r,x)\),在點分樹上 \(x\) 的祖先中找到深度最小的點 \(pa\),且滿足 \(x\) 只經過編號 \([l,r]\) 內的點在原樹上能到達 \(pa\) 。記一下每個點 \(i\) 到虛樹祖先的路徑上所經過的節點編號最小/大值就可以輕松求得(分別記為 \(d_{min}(i,j)\) 和 \(d_{max}(i,j)\))。
分析可知:\(x\) 只經過編號 \([l,r]\) 內的點所在的連通塊被完全包含在了 \(subtree(pa)\) 中(虛子樹)。我們把本次詢問放到 \(pa\) 節點處,最后再統一離線處理。
枚舉虛樹上的點 \(rt\),處理該節點處的詢問時,對於任意一個 \((l,r,x)\),滿足 \(l\leqslant d_{min}(i,rt)\) 且 \(d_{max}(i,rt)\leqslant r\) 的 \(i\) 即為與 \(x\) 在同一連通塊內的點,現需要統計這些點的顏色種類。
顯然是個偏序問題,把詢問和節點信息放一起按 \(l\) 排序,指針從右往左掃,同時記錄每種顏色節點右端點最小的位置,再開一棵樹狀數組維護每個位置上的數量,便可直接查詢了。
空間復雜度:\(O(n\log n)\) 。
時間復雜度:\(O(n\log^2 n)\) 。
(2).【Code】
struct Query{int l,r,id,co;inline bool operator<(const Query &O)const{return l!=O.l?l>O.l:id<O.id;}};vector<Query>Q[N];
inline void getdis(Re x,Re rt,Re fa,Re d1,Re d2){//d1:最小值 d2:最大值
d1=min(d1,x),d2=max(d2,x),Q[rt].push_back((Query){d1,d2,0,A[x]});
frt[x][++gs[x]]=rt,fd1[x][gs[x]]=d1,fd2[x][gs[x]]=d2;
for(Re i=head[x],to;i;i=a[i].next)if(!vis[to=a[i].to]&&to!=fa)getdis(to,rt,x,d1,d2);
}
inline void change(Re x,Re l,Re r,Re id){//插入第id次詢問l,r,x
for(Re i=1;i<=gs[x];++i)
if(l<=fd1[x][i]&&fd2[x][i]<=r){Q[frt[x][i]].push_back((Query){l,r,id,0});break;}
}
struct BIT{
int C[N];
inline void CL(Re x){while(x<=n)C[x]=0,x+=x&-x;}
inline void add(Re x,Re v){while(x<=n)C[x]+=v,x+=x&-x;}
inline int ask(Re x){Re ans=0;while(x)ans+=C[x],x-=x&-x;return ans;}
}TR;
inline int GetAns(){
for(Re i=1;i<=100000;++i)mir[i]=inf;
for(Re rt=1;rt<=n;++rt){//枚舉點分樹上的點
sort(Q[rt].begin(),Q[rt].end());
for(IT it=Q[rt].begin();it!=Q[rt].end();++it)
if(it->id)Ans[it->id]=TR.ask(it->r);//查詢
else if(it->r<mir[it->co])TR.add(mir[it->co],-1),TR.add(mir[it->co]=it->r,1);//節點信息,更新此顏色的最小右端點
for(IT it=Q[rt].begin();it!=Q[rt].end();++it)
if(!(it->id))TR.CL(mir[it->co]),mir[it->co]=inf;
}
}
6.【Iqea】
\(\text{Iqea}\) \(\text{[CF936E]}\)
【題目大意】 二維平面上給出若干個點的坐標,在這些點處打好地基(保證為一個四連通塊,且不會出現封閉的空地)。有兩種操作:在 \((x,y)\) 處建造商店,詢問離 \((x,y)\) 最近的商店與 \((x,y)\) 的距離大小。兩點距離定義為只經過有地基的點的最短路徑長度,相鄰兩個格子距離為 \(1\) 。保證每次給出的坐標處一定有地基。
(1).【分析】
一只神薙。
首先是非常巧妙的建圖:每一行分開看,把同一行的若干個聯通塊分別縮成點,然后向四周相鄰的點連邊,由於沒有封閉的空地,這樣連出來一定是棵樹。
支持加入關鍵點,查詢距離最近的關鍵點,建點分樹?
似乎還沒做完:兩點之間的最短距離要如何在樹上表示呢?
看圖:

任選 \(a,b\) 路徑上一個縮成點的小連通塊 \(m\) 作為中介(如圖中黃色框),先分別求出 \(a,b\) 移動到中介的最短路徑 \(d_a(m),d_b(m)\),並記錄它們到達中介時的縱坐標 \(y_a(m),y_b(m)\)(在圖中表現為 \(1\) 號和 \(8\) 號位置),則 \(dis(a,b)=d_a(m)+d_b(m)+|y_a(m)-y_b(m)|\),即 \(\min\{d_a(m)+d_b(m)+y_a(m)-y_b(m),d_a(m)+d_b(m)+y_b(m)-y_a(m)\}\) 。
現在點分樹就可以輕松維護答案了,設:
\(f_1(i,j)=\sum_{x\in subtree(i),y_x(i)\leqslant j}d_x(i)-y_x(i)\)
\(g_1(i,j)=\sum_{x\in subtree(i),y_x(i)\geqslant j}d_x(i)+y_x(i)\)
則有 \(ans(x,k)=\) \(\min_{i\in fatree(x)}\) \(\min\{d_x(i)+y_x(i)+f_1(i,y_x(i)),d_x(i)-y_x(i)+g_1(i,y_x(i))\}\)
二維的 \(f_1,g_1\) 用樹狀數組維護。
空間復雜度:\(O(n\log n)\) 。
時間復雜度:\(O(n\log n+q\log^2 n)\) 。
(2).【Code】
int ip_O,n,o,x,y,T,op,MaxX,X[N],ip[N],Yl[N],Yr[N],idl[N],idr[N],head[N];vector<int>V[N];map<int,int>id[N];
struct QAQ{int to,next;}a[N<<1];//外層的這個是縮圖所得到的樹
inline void add(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
struct Tree{
int o,head[N];
struct QAQ{int to,next;}a[N<<2];
inline void add_(Re x,Re y){a[++o].to=y,a[o].next=head[x],head[x]=o;}
inline void add(Re x,Re y){add_(x,y),add_(y,x);}
}T0;//T0:以二維平面上的連通性建成的圖
inline void get_tree(){//縮圖建樹
for(Re i=1;i<=n;++i)in(x),in(y),MaxX=max(MaxX,x),V[x].push_back(y),id[x][y]=i;
for(Re x=1;x<=MaxX;++x)if(!V[x].empty()){
sort(V[x].begin(),V[x].end());
Re last=-1;idl[x]=ip_O+1;
for(Re J=0,SZ=V[x].size(),y;J<SZ;last=V[x][J],++J){
if(last+1!=(y=V[x][J]))ip[id[x][y]]=++ip_O,X[ip_O]=x,Yl[ip_O]=Yr[ip_O]=y;
else ip[id[x][y]]=ip_O,Yr[ip_O]=y,T0.add(id[x][y],id[x][y-1]);
if(id[x-1].find(y)!=id[x-1].end())T0.add(id[x-1][y],id[x][y]);
}
idr[x]=ip_O;
if(V[x-1].empty())continue;
Re k=idl[x-1];
for(Re j=idl[x];j<=idr[x];++j){
while(k<=idr[x-1]&&Yr[k]<Yl[j])++k;
for(Re k_=k;k_<=idr[x-1]&&Yl[k_]<=Yr[j];++k_)add(k_,j),add(j,k_);
}
}
}
struct QWQ{
int d,y;QWQ(Re D=0,Re Y=0){d=D,y=Y;}
inline bool operator<(const QWQ &O)const{return d<O.d;}
};
struct BIT{
int n,op;vector<int>C;//op=0:前綴 op=1:后綴(把詢問、查詢里的x都翻轉一下就是了)
inline void build(Re N){n=N,C.push_back(inf);while(N--)C.push_back(inf);}//這里就不用resize了,因為要初始化為inf
inline void add(Re x,Re v){if(op)x=n-x+1;while(x<=n)C[x]=min(C[x],v),x+=x&-x;}
inline int ask(Re x){if(op)x=n-x+1;Re ans=inf;while(x)ans=min(ans,C[x]),x-=x&-x;return ans;}
}TR1[N],TR2[N];
int rt,sum,gs[N],vis[N],maxp[N],size[N],frt[N][22];QWQ fdis[N][22];
inline void getrt(Re x,Re fa){
size[x]=Yr[x]-Yl[x]+1,maxp[x]=0;//注意size的初始化不是1
for(Re i=head[x],to;i;i=a[i].next)if(!vis[to=a[i].to]&&to!=fa)
getrt(to,x),size[x]+=size[to],maxp[x]=max(maxp[x],size[to]);
maxp[x]=max(maxp[x],sum-size[x]);if(maxp[x]<maxp[rt])rt=x;
}
int Q[N],pan[N];QWQ dis[N];
inline void getdis0(Re rt){//bfs
Re h=1,t=0;
for(Re i=Yl[rt];i<=Yr[rt];++i)pan[Q[++t]=id[X[rt]][i]]=rt,dis[Q[t]]=QWQ(0,i-Yl[rt]+1);
while(h<=t){
Re x=Q[h++];
for(Re i=T0.head[x],to;i;i=T0.a[i].next)
if(pan[to=T0.a[i].to]!=rt&&!vis[ip[to]])
dis[to]=QWQ(dis[x].d+1,dis[x].y),pan[Q[++t]=to]=rt;
}
}
inline void getdis(Re x,Re rt,Re fa){
frt[x][++gs[x]]=rt;
for(Re i=Yl[x];i<=Yr[x];++i)fdis[id[X[x]][i]][gs[x]]=dis[id[X[x]][i]];//距離在bfs中已獲得
for(Re i=head[x],to;i;i=a[i].next)if(!vis[to=a[i].to]&&to!=fa)getdis(to,rt,x);
}
inline void sakura(Re x){
Re now=sum;vis[x]=1,getdis0(x),getdis(x,x,0);
TR1[x].build(Yr[x]-Yl[x]+1),TR2[x].build(Yr[x]-Yl[x]+1),TR2[x].op=1;//樹狀數組的使用范圍為[1,Yr-Yl+1]
for(Re i=head[x],to;i;i=a[i].next)if(!vis[to=a[i].to])
sum=size[to]>size[x]?now-size[x]:size[to],maxp[rt=0]=inf,getrt(to,0),sakura(rt);
}
inline void change(Re y){
Re x=ip[y];
for(Re i=gs[x];i;--i)
TR1[frt[x][i]].add(fdis[y][i].y,fdis[y][i].d-fdis[y][i].y),
TR2[frt[x][i]].add(fdis[y][i].y,fdis[y][i].d+fdis[y][i].y);
}
inline int ask(Re y){
Re x=ip[y],ans=inf;
for(Re i=gs[x];i;--i)
ans=min(ans,fdis[y][i].d+fdis[y][i].y+TR1[frt[x][i]].ask(fdis[y][i].y)),
ans=min(ans,fdis[y][i].d-fdis[y][i].y+TR2[frt[x][i]].ask(fdis[y][i].y));
return ans;
}
7.【大毒瘤】
相信您已經積累了足夠的實力,下面我們來看一道果題吧QAQ
紫荊花之戀 \(\text{[WC2014] [P3920]}\)
四:【總結】
模板及例題 \(1.1,1.2\) 都是靠數據結構維護貢獻,例 \(2\) 則換成了 \(\text{STL}\) 。套娃行為所導致的的碼量增加以及大常數都是值得關注的問題。
例 \(3,4\) 主要是按照容斥套路推式子,相對比較好掌握,但細節較多,要注意統計貢獻時補充不漏。
例 \(5\) 難點在於利用點分樹的特殊性質轉離線。點分樹各種神奇的特性,對應到不同的題上就會有各種神奇的解法。如果沒有強大的瞎蒙猜結論能力,就多刷刷題吧,見多識廣總沒有壞處的。
例 \(6\) 不光要會神仙建圖,還要想辦法用點分樹能維護的東西來表示兩點距離。這道題有效地提醒了我們:點分樹有着不錯的靈活性,並非只有那個一成不變的模板,所以不要死記硬背啊...
Q: 話說點分樹能搞可持久化嗎?
A: 雖然聽起來比較毒瘤,但不瞞您說,還真可以(具體見 可持久化點分樹)



