点分治
算法学习
前言
解决什么样的问题: 树上静态路径计数,往往题目与树上路径长度有关。
原理: 以任意一个点为根,树上的路径被分为两种:过根节点的/不过根节点的。点分支在树上进行分治,每次找到重心,将一棵树变成若干棵子树。统计第一类路径,并递归到子树进行点分治,这样第二类路径就包括在了子树的第一类路径和第二类路径里。
写法:
- 维护 d i s dis dis 表示已经存储的若干棵子树的路径, t m p tmp tmp 表示 v v v 子树的路径。
- 通过 d i s dis dis 和 t m p tmp tmp 的组合,即可求出所有跨根路径。
时间复杂度: 基础时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 。
完整模板
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+100;
vector<int>G[N];
long long n,dis[N],ans=0,del[N];
//1.找重心rt
int siz[N],maxz[N],rt;
void getRt(int u,int fa){
siz[u]=1;
for(int v:G[u]){
if(v==fa||del[v])continue;
getRt(v,u);
siz[u]+=siz[v];
maxz[u]=max(maxz[u],siz[v]);
}
maxz[u]=max(maxz[u],n-siz[u]);
if(maxz[u]<maxz[rt])rt=u;
}
//2.统计以u为根的第一类路径
//2.1求以u为根的子树距离
vector<int>tmp;
void getDis(int u,int fa,int d){
tmp.push_back(d);
for(int v:G[u]){
if(v==fa||del[v])continue;
getDis(v,u,d+1);
}
}
//2.2两两子树合并计算
void calc(int u){
memset(dis,0,sizeof(dis));
for(int v:G[u]){
if(del[v])continue;
tmp.clear();
getDis(v,u,1);
//temp与dis组合答案
for(int d:tmp)dis[d]++;
}
}
void dfz(int u){
calc(u);
//calc(u,1);
del[u]=1;
for(int v:G[u]){
if(del[v]==0){
rt=0;
//calc(u,-1);
getRt(v,0);//找到子树的重心
dfz(rt);//计算子树
}
}
}
int main(){
int u,v;
cin>>n;
for(int i=1;i<n;i++){
cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
maxz[0]=1e9;
getRt(1,0);
dfz(rt);
cout<<ans;
}
例题练习
例题1:P2634 统计路径长度为 3 3 3 的整数倍的路径
题目描述: 给定一棵树,要求统计路径长度为 3 3 3 的整数倍的路径数量。
问题分析: a n s = ∑ s i g n × ( d i s [ 0 ] + d i s [ 0 ] + d i s [ 1 ] × d i s [ 2 ] × 2 ) ans=\sum sign\times (dis[0]+dis[0]+dis[1]\times dis[2]\times 2) ans=∑sign×(dis[0]+dis[0]+dis[1]×dis[2]×2) 。加上 u u u 节点的答案,减去 $\sum $ v v v 的答案,即可得到跨子树的答案。
void getdis(int u,int fa,int sumdis){
dis[sumdis%3]++;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa||del[v])continue;
getdis(v,u,sumdis+e[i].w);
}
}
void calc(int u,int sign,int fir){
dis[1]=dis[2]=dis[0]=0;
getdis(u,0,fir%3);
if(sign==1)ans+=dis[1]*dis[2]*2+dis[0]*dis[0];
else ans-=dis[1]*dis[2]*2+dis[0]*dis[0];
}
void dfz(int u){
calc(u,1,0);//计算子树
del[u]=1;
for(int v:G[u]){
if(del[v]==0){
rt=0;
calc(v,-1,e[i].w);//删去v子树
getRt(v,0);//找到子树的重心
dfz(rt);//计算子树
}
}
}
例题2:P3806 统计是否有长度为 k 的路径(k<=1e7)
题目描述: 给定一棵树。 m m m 次询问,每次询问求是否有长度为 k i k_i ki 的路径。
问题分析: 将问题离线到 q [ m ] q[m] q[m] 里。组合路径时: a n s [ i ] = d i s [ q [ i ] − t m p [ j ] ] ans[i]=dis[q[i]-tmp[j]] ans[i]=dis[q[i]−tmp[j]] 。
vector<int>tmp;
void getdis(int u,int fa,int d){
if(d>1e7)return;
tmp.push_back(d);
for(int v:G[u]){
if(v==fa||del[v])continue;
getdis(v,u,d+e[i].w);
}
}
vector<int>cl;
void calc(int u){
for(int v:cl)dis[v]=0;
dis[0]=1;
cl.clear();
for(int v:G[u]){
if(del[v])continue;
tmp.clear();
getdis(v,u,e[i].w);
//temp与dis组合答案
for(int d:tmp){
for(int k=1;k<=m;k++){
if(q[k]-d>=0)ans[k]|=dis[q[k]-d];
}
}
for(int d:tmp){
dis[d]=1;
cl.push_back(d);
}
}
}
void dfz(int u){
calc(u);
//calc(u,1);
del[u]=1;
for(int v:G[u]){
if(del[v]==0){
root=0;
//calc(u,-1);
getRt(v,0);//找到子树的重心
dfz(rt);//计算子树
}
}
}
例题3:P4178 统计路径长度 <=k 的路径数量(k<=2e4)
题目描述: n n n 个点构成一棵树,边带边权。求路径长度 < = k <=k <=k 的路径数量。
问题分析:
- 树上启发式合并只有求每个点为根跨子树的路径。其父节点往上的那颗子树没法完整算进去。
- 这时候就可以用点分支了。
- 按模板统计每棵子树的
d
i
s
dis
dis 即可,统计时:
ans+=query(goal-tmp[j])+1
。其中 q u e r y ( x ) query(x) query(x) 表示 d i s dis dis 中 < = x <=x <=x 的数量。 - 用树状数组维护 d i s dis dis 即可。
vector<int>tmp;
void getdis(int u,int fa,int d){
if(d>R)return;
tmp.push_back(d);
for(auto [v,w]:G[u]){
if(v==fa||del[v])continue;
getdis(v,u,d+w);
}
}
void calc(int u){
for(int i=1;i<=R;i++)dis[i]=0;
for(auto [v,w]:G[u]){
if(del[v])continue;
tmp.clear();
getdis(v,u,w);
//temp与dis组合答案
for(int d:tmp){
if(goal-d>=0)ans+=query(goal-d)+1;
}
for(int d:tmp)update(d,1);
}
}
void dfz(int u){
calc(u);
del[u]=1;
for(auto [v,w]:G[u]){
if(del[v]==0){
root=0;
getroot(v,0);//找到子树的重心
dfz(root);//计算子树
}
}
}