點分治
點分治,是一個很簡單非常常見的數據結構
她是一種處理樹上路徑問題的工具,舉個栗子:
給定一棵樹和一個整數k,求樹上邊數等於k的路徑有多少條
當樹的節點數比較多的時候,就不能使用暴力了,我該怎么辦
就要用點分治
原理

如圖,我們在這棵樹上選出一個root,那路徑一共有三種情況:
1.在紅子樹中
2.在黑子樹中
3.一半在紅子樹,一半在黑子樹,要過root,拼成一條完整的路徑
分類討論,不存在的qaq,或許這輩子我也不會分類討論
仔細想一下,實際情況1,2都珂以看做情況3,如圖將答案中一點變成root,就成了情況3

好的上面只是思想,好像很虛空
我們需要實現
選根
選根的過程實際就是一個樹形dp
選root是非常重要的,選不好會使復雜度爆炸
想想會發現這個根最好是樹的重心
所以一個簡單的樹形dp就能搞定
inline void getroot(register int u,register int fa)
{
size[u]=1;
int num=0;
for(register int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa||vis[v])
continue;
getroot(v,u);
size[u]+=size[v];
num=Max(num,size[v]);
}
num=Max(num,sizee-size[u]);
if(mx>num)
mx=num,rt=u;
}
因為之后的分治過程還需要對子樹單獨找重心,所以代碼中有vis,但是開始對整棵樹無影響
分治
這才是點分治的精華qaq
根據代碼來理解
inline ll devide(register int u)
{
ll res=solve(u,0); //把當前節點的答案加上去
vis[u]=true; //把節點標記,防止陷入死循環
int totsize=sizee;
for(register int i=head[u];i;i=e[i].next)
{
//分別處理每一棵子樹
int v=e[i].to;
if(vis[v])
continue;
res-=solve(v,e[i].v); //容斥原理,下面說
rt=0,sizee=size[v]>size[u]?totsize-size[u]:size[v],mx=inf;
//把所有信息更新,遞歸進子樹找重心,並繼續分治
getroot(v,0);
res+=devide(rt);
}
return res;
}
大部分都應該比較好理解,除了ans-=slove(v,1)這句
我們先看一種情況:

在路徑A->Root和B->Root合並時,這種情況顯然是不合法的
所以要減去一些路徑
完整代碼
#include <bits/stdc++.h>
#define N 50005
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
inline int read()
{
register int x=0,f=1;register char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
inline void write(register ll x)
{
if(!x)putchar('0');if(x<0)x=-x,putchar('-');
static int sta[36];int tot=0;
while(x)sta[tot++]=x%10,x/=10;
while(tot)putchar(sta[--tot]+48);
}
inline int Max(register int a,register int b)
{
return a>b?a:b;
}
struct node{
int to,next,v;
}e[N<<1];
int head[N],tot=0;
inline void add(register int u,register int v,register int k)
{
e[++tot]=(node){v,head[u],k};
head[u]=tot;
}
int n,k;
int size[N];
int rt,sizee,mx;
bool vis[N];
ll ans=0;
ll d[N],q[N],l,r;
inline void getroot(register int u,register int fa)
{
size[u]=1;
int num=0;
for(register int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa||vis[v])
continue;
getroot(v,u);
size[u]+=size[v];
num=Max(num,size[v]);
}
num=Max(num,sizee-size[u]);
if(mx>num)
mx=num,rt=u;
}
inline void getdis(register int u,register int fa)
{
q[++r]=d[u];
for(register int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa||vis[v])
continue;
d[v]=d[u]+e[i].v;
getdis(v,u);
}
}
inline ll solve(register int u,register int val)
{
r=0;
d[u]=val;
getdis(u,0);
ll sum=0;
l=1;
sort(q+1,q+r+1);
while(l<r)
{
if(q[l]+q[r]<=k)
sum+=r-l,++l;
else
--r;
}
return sum;
}
inline ll devide(register int u)
{
ll res=solve(u,0);
vis[u]=true;
int totsize=sizee;
for(register int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(vis[v])
continue;
res-=solve(v,e[i].v);
rt=0,sizee=size[v]>size[u]?totsize-size[u]:size[v],mx=inf;
getroot(v,0);
res+=devide(rt);
}
return res;
}
int main()
{
n=read();
for(register int i=1;i<n;++i)
{
int u=read(),v=read(),w=read();
add(u,v,w),add(v,u,w);
}
k=read();
sizee=n,mx=inf;
getroot(1,rt=0);
ans=devide(rt);
write(ans);
return 0;
}
動態點分治
一般,點分治只能處理靜態的問題
但是,毒瘤的出題人加上了修改操作該怎么辦qaq?
每修改一次做一次點分治?復雜度直接飛天
考慮一下,修改操作修改的是點權(不是樹的結構,樹的結構的話請找lct同學)
樹的重心不會改變
先給出點分樹的定義qaq:
點分治時每一層的重心連出的一個深度為logn的樹

我們假設已經處理完了所有經過點1的路徑,然后遞歸進子樹繼續點分,那么實際上原樹被拆成了這么兩棵樹,兩個重心分別為2和6

那么把第一層的重心和第二層的重心給連接起來(用紅色表示)

然后我們繼續進行點分,我們已經把經過點2和點6的所有路徑都已經處理完了,那么子樹又會繼續拆分

然后因為子樹大小只有1,重心就是他們自己,繼續和上一層的重心連邊

一棵點分樹就這樣建好了
在代碼實現上實際只有一點點小的變動
inline void devide(register int u)
{
vis[u]=true;
int totsize=sizee;
for(register int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(vis[v])
continue;
rt=0,sizee=size[v]>size[u]?totsize-size[u]:size[v],mx=inf;
getroot(v,0);
fa[rt]=u;
devide(rt);
}
return res;
}
那么每一次修改,只要在點分樹里不斷往上跳,就能夠維護整棵樹的信息了
好像十分抽象,結合一道例題來講或許會更好
題意簡介:
給你了一棵樹,每個節點有個權值(0/1),一開始所有點全是0
有一下兩種操作:
C(Change)i,把i這個節點的權值取反
G(Game)查詢距離最遠的兩個權值為0的點的距離
題解先咕咕咕