LGP3806 [LG TPLT] 点分治·I 学习笔记

LGP3806 [LG TPLT] 点分治·I 学习笔记

Luogu Link

前言

点分治适合处理大规模的树上路径信息问题。尤其是与树形态无关的。

题意简述

给定一棵有 \(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 的搜索起点。

posted @ 2025-07-24 16:39  矞龙OrinLoong  阅读(5)  评论(0)    收藏  举报