LGP3806 [LG TPLT] 点分治·I 学习笔记
LGP3806 [LG TPLT] 点分治·I 学习笔记
前言
点分治适合处理大规模的树上路径信息问题。尤其是与树形态无关的。
题意简述
给定一棵有 \(n\) 个点的树,\(m\) 次询问树上距离为 \(k_i\) 的点对是否存在。
\(n\le 10^4\),\(m\le 10^2\)。
做法解析
暴力做法是枚举所有点对,复杂度在 \(O(n^2)\) 起步,这太糖了,考虑优化。
点分治的思想是,如果你指定一个点为根,处理其它点到当前根的路径情况,就可以考察经过当前根的路径。我们指定一个根之后,这棵树会被当前的根割成两半,我们在每一半的树里面再递归地这么做就可以解决问题了。而当我们每次都选取子结构的重心作为根的时候,就可以保证递归的层数是 \(O(\log n)\) 的。我们就可以在更优的复杂度里(通常为 \(O(n\log n)\) 或 \(O(n\log^2n)\))解决问题。
你可能忘了树的重心的定义。其定义为:能使得删除该节点后裂出的所有子树中大小的最大值最小的结点。
具体来说怎么做呢?对于每个子结构或者说子问题而言,我们显然要先找出它的重心。这一步的代码如下。
void getroot(int u,int f,int tot){
siz[u]=1,hsw[u]=0;
for(auto [v,w] : Tr[u]){
if(v==f||vis[v])continue;
getroot(v,u,tot);
siz[u]+=siz[v],maxxer(hsw[u],siz[v]);
}
maxxer(hsw[u],tot-siz[u]);
if(!rt||hsw[u]<hsw[rt])rt=u;
}
以此为根,我们就来统计当前子问题里面所有点到根的路径情况了。得到所有的dis之后,我们对于每个询问,在dis上双指针即可。
但是这可能出现一个问题:有可能有两个 \(\text{lca}\) 不是根的结点它们凑出了目标距离,而这是错的。所以我们要特判一下。
代码实现
#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=1e4+5;
int N,M,X,Y,Z,Q[MaxN],ans[MaxN];
struct edge{int to,co;};vector<edge> Tr[MaxN];
void addudge(int u,int v,int w){
Tr[u].push_back({v,w});
Tr[v].push_back({u,w});
}
int siz[MaxN],hsw[MaxN],vis[MaxN];
int getroot(int u,int f,int tot){
siz[u]=1,hsw[u]=0;int t=0,x;
for(auto [v,w] : Tr[u]){
if(v==f||vis[v])continue;
x=getroot(v,u,tot);if(hsw[x]<hsw[t])t=x;
siz[u]+=siz[v],maxxer(hsw[u],siz[v]);
}
maxxer(hsw[u],tot-siz[u]);
if(hsw[u]<hsw[t])t=u;return t;
}
struct anob{int p,b,d;}dis[MaxN];int dcnt;
bool cmpd(anob a,anob b){return a.d<b.d;}
void getdis(int u,int f,int cdis,int anc){
dis[++dcnt]={u,anc,cdis};
for(auto [v,w] : Tr[u]){
if(v==f||vis[v])continue;
getdis(v,u,cdis+w,anc);
}
}
void calc(int u){
dcnt=0,dis[++dcnt]={u,u,0};
for(auto [v,w] : Tr[u])if(!vis[v])getdis(v,u,w,v);
sort(dis+1,dis+dcnt+1,cmpd);
for(int i=1;i<=M;i++){
if(ans[i])continue;
for(int l=1,r=dcnt;l<r;){
auto [lp,lb,ld]=dis[l];
auto [rp,rb,rd]=dis[r];
if(ld+rd>Q[i]){r--;continue;}
if(ld+rd<Q[i]){l++;continue;}
if(lb==rb){rd==dis[r-1].d?r--:l++;continue;}
ans[i]=1;break;
}
}
}
void solve(int u,int s){
vis[u]=1,calc(u);
for(auto [v,w] : Tr[u]){
if(vis[v])continue;
int vtot=siz[v]<siz[u]?siz[v]:siz[s]-siz[u];
int rt=getroot(v,0,vtot);solve(rt,v);
}
}
void asolve(){hsw[0]=N;solve(getroot(1,0,N),1);}
int main(){
readis(N,M);
for(int i=1;i<N;i++)readis(X,Y,Z),addudge(X,Y,Z);
for(int i=1;i<=M;i++)readi(Q[i]);
asolve();for(int i=1;i<=M;i++)puts(ans[i]?"AYE":"NAY");
return 0;
}
后记
(代码实现已做相应更新)
在一种常见的写法中,solve() 中直接 rt=getroot(v,0,siz[v]); 其实并不是很严谨。虽然只要你找重心是比较 siz[u] 和 tot-siz[u] 那复杂度本身就没有问题,然而,有时候我们的问题需要用到点分树结构上每个子树的准确大小,这么写就会导致其求出来偏大,详情见此博客。
虽然说,从复杂度分析角度来看,你多开大一点点空间确实不会有什么大事,但我个人认为这是不优美的。解决办法很简单。我们要特判的就是满足 siz[v]>siz[u] 的 v,它的 tot 应该是 siz[s]-siz[u],其中 s 是对于搜出当前根 u 的搜索起点。
浙公网安备 33010602011771号