点分治&&点分树
点分治
前置芝士:树的重心
树的重心指对于一棵无根树上的一点,其分割开的若干子树大小的最大值最小。
一般用 DFS 求解树的重心。初始时,
void getrt(int u,int fno){
int s=0;
siz[u]=1;
for(int i=head[u],v;i;i=e[i].to){
if((v=e[i].v)==fno||del[v]) continue;
getrt(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sm-siz[u]);
if(s<mxs) mxs=s,rt=u;
}
点分治
点分治,又名淀粉质,是在树上进行静态统计的算法。这里的静态统计一般是树剖维护不了的,所以我们需要点分治。但在实际应用中,树剖确实能解决很多点分治的题,码量还更小。
点分治的思想是将一棵树拆成许多棵子树进行处理。比如,对于一条路径,可以将其分成两类:经过根节点和不经过根节点的路径。对于前者,可以预处理出每个点到根的路径,然后
如果是均衡的树,查询一次是
P3806 【模板】点分治 1
题意:给定一棵
个点的树,每次询问树上距离为 的点对是否存在。
点分治板子题,分四步走:
- 找出树的重心做根,
getrt()
; - 求出子树各点到根的距离,
getdis()
; - 统计当前子树答案,
calc()
; - 分治各个子树,重复上述操作,
divide()
。
// 核心代码
void getrt(int u,int fno){
siz[u]=1;
int s=0;
for(int i=head[u],v;i;i=e[i].to){
if((v=e[i].v)==fno||del[v]) continue;
getrt(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sum-siz[u]);
if(s<mxs) mxs=s,rt=u;
}
void getdis(int u,int fno){
dis[++cnt]=d[u];
for(int i=head[u],v;i;i=e[i].to){
if((v=e[i].v)==fno||del[v]) continue;
d[v]=d[u]+e[i].w;
getdis(v,u);
}
}
void calc(int u){
judge[0]=1;
int p=0;
for(int i=head[u],v;i;i=e[i].to){
if(del[v=e[i].v]) continue;
cnt=0;
d[v]=e[i].w;
getdis(v,u);
for(int j=1;j<=cnt;++j)
for(int k=1;k<=m;++k)
if(ask[k]>=dis[j])
ans[k]|=judge[ask[k]-dis[j]];
for(int j=1;j<=cnt;++j)
if(dis[j]<MAXK) judge[q[++p]=dis[j]]=1;
}
for(int i=1;i<=p;++i) judge[q[i]]=0;
}
void divide(int u){
del[u]=1;
calc(u);
for(int i=head[u],v;i;i=e[i].to){
if(del[v=e[i].v]) continue;
mxs=sum=siz[v];
getrt(v,0);
divide(rt);
}
}
稍微解释一下 calc()
函数:前面提到计算经过根节点的路径时需要注意排除不合法情况,所以我们用
P4178 Tree
题意:给定一棵
个节点的树,边有边权,求树上距离小于等于 的点对数量。
仍然点分治四步走。和板题不一样的地方在于边有边权,以及统计的是数量而不是是否存在。观察到既然是小于等于,可以想到树状数组维护前缀和。于是把所有出现过的距离扔进树状数组里,查询的时候查询前缀和即可。需要注意的是,最后不能忘了统计以根节点为端点的路径。
// 这是我以前的码风
void calc(int u) {
queue<int> q;
for (auto vv : g[u]) {
int v = vv.first, w = vv.second;
if (del[v]) continue;
d[v] = w;
cnt = 0;
getdis(v, u);
for (int j = 1; j <= cnt; ++j) {
if (dis[j] > K) continue;
ans += ask(K - dis[j]);
}
for (int j = 1; j <= cnt; ++j) {
if (dis[j] > K) continue;
q.emplace(dis[j]);
++judge[dis[j]];
mdf(dis[j], 1);
}
}
while (!q.empty()) {
if (q.front() <= K) ans += judge[q.front()];
mdf(q.front(), -judge[q.front()]);
judge[q.front()] = 0;
q.pop();
}
}
P3085 [USACO13OPEN] Yin and Yang G
其实它更广为人知的名字是采药人的路径。
题意:给出一棵
个点的树,每条边为黑色或白色。问满足以下条件的路径条数:路径上存在一个不是端点的点,使得两端点到该点的路径上两种颜色的边数都相等。
首先容易想到的是把边权变为
设一个点
我们定义
对于答案的求解是本题的难点,设:
表示对于当前根,现在正在遍历,距离根的距离为 ,找不到/找得到中转点的点的个数; 表示对于当前根,以前遍历过,距离根的距离为 ,找不到/找得到中转点的点的个数。
对于
然后考虑用
ans+=f[N][0]*(g[N][0]-1);
for(int j=-mxd;j<=mxd;++j)
ans+=f[N+j][1]*g[N-j][1]+f[N+j][1]*g[N-j][0]+f[N+j][0]*g[N-j][1];
首先计算根是中转点的情况,此时找不到中转点的点也可以配对,同时根是端点的情况多算一遍要减去。
对于一组距离为相反数的点对,能否配对取决于这对点中的任意一个能否找到中转点。
剩下的就不是难点了。
P3714 [BJOI2017] 树的难题
对于一个点为根,我们可以很容易计算出
点分树(动态点分治)
点分树和点分治的关系仅仅在于建树时运用了点分治的思想。又因其主要解决带修问题,故又名动态点分治。
点分树
定义:利用点分治,以每个子树的重心做根,重构出来的一棵树。
实话说,点分树和原树的形态没有任何关系。有什么用?主要是两个性质:
- 树高严格小于等于
! - 原树两点 LCA 在点分树两点的路径上!
第一条性质最为重要,它可以让我们做出很多不可思议的事情。
比如,在点分树上做
比如,对点分树的每一个节点开一个 vector 存储子树所有节点的信息,空间复杂度却同样是
有的问题我们并不是特别关注树的形态特点,比如路径问题、连通块问题、关键点问题等,以路径问题为例,我们并不一定要算出两点的 LCA 才能计算距离,我们只需找到路径上的一个分割点即可。
点分树题的一般解题技巧是开两个数据结构(可以是数组、vector、线段树等),分别维护对
点分树的常数巨大,写点分树必须注意常数问题。
P6329 【模板】点分树 | 震波
题意:给定一棵
个点的树,点有点权。每次查询距离点 不超过 的点的点权和。带修,强制在线。
建出点分树,记
第一步是用倍增/树剖/ST 表求出树上两点间距离。建议使用树剖,因为码量小还跑得快(跑不满的
第二步,开两棵动态开点线段树,设
查询时,暴力跳父亲,当查询距离节点
修改时,暴力跳父亲修改即可。
struct{
... // 树剖求LCA和树上两点距离,不再展示
}L;
struct{
// 单加区查的动态开点线段树
#define lp st[p].lc
#define rp st[p].rc
int rt[MAXN],tot;
struct SegTree{
int lc,rc,c;
}st[MAXN<<5]; // 空间复杂度由数据规模决定
void chg(int x,int k,int s,int t,int&p){
if(!p) p=++tot;
if(s==t) return st[p].c+=k,void();
int mid=(s+t)>>1;
if(x<=mid) chg(x,k,s,mid,lp);
else chg(x,k,mid+1,t,rp);
st[p].c=st[lp].c+st[rp].c;
}
int query(int l,int r,int s,int t,int p){
if(!p) return 0;
if(l<=s&&t<=r) return st[p].c;
int mid=(s+t)>>1,res=0;
if(l<=mid) res+=query(l,r,s,mid,lp);
if(mid<r) res+=query(l,r,mid+1,t,rp);
return res;
}
// 建树略有不同,wc是根,d是距离
// 相当于给线段树上距离根节点d的位置加上了点权w[u]
void build(int u,int fno,int wc,int d){
chg(d,w[u],0,n,rt[wc]);
for(int i=head[u];i;i=e[i].to)
if(e[i].v^fno&&!del[e[i].v])
build(e[i].v,u,wc,d+1);
}
}sg,ch;
struct{
int sum,mxs,rt,siz[MAXN];
int dis[MAXN][21],dep[MAXN],fa[MAXN];
void getrt(int u,int fno){
siz[u]=1;
int s=0;
for(int i=head[u],v;i;i=e[i].to){
if((v=e[i].v)==fno||del[v]) continue;
getrt(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sum-siz[u]);
if(s<mxs) mxs=s,rt=u;
}
void build(int u){
del[u]=1;
// sg建树时根节点是u,维护的信息是到u的信息
sg.build(u,0,u,0);
for(int i=head[u],v;i;i=e[i].to){
if(del[v=e[i].v]) continue;
sum=mxs=siz[v];
getrt(v,0);
// ch建树时根节点是子树重心,但维护的距离是到根节点父亲的距离
ch.build(v,0,rt,1);
fa[rt]=u;
dep[rt]=dep[u]+1;
build(rt);
}
}
void init(){
// 找重心
sum=mxs=n;
getrt(1,0);
build(rt);
// 树剖预处理
L.dfs(1,0);
L.dfs2(1,1);
// 预处理dis,dis[i][j]表示点分树上i的j级祖先在原树上的距离
for(int i=1;i<=n;++i)
for(int j=i;j;j=fa[j])
dis[i][dep[i]-dep[j]]=L.getdis(i,j);
}
int query(int x,int y){
int res=sg.query(0,y,0,n,sg.rt[x]);
for(int i=x,d;fa[i];i=fa[i]){
d=dis[x][dep[x]-dep[fa[i]]];
res+=sg.query(0,y-d,0,n,sg.rt[fa[i]]);
res-=ch.query(0,y-d,0,n,ch.rt[i]);
}
return res;
}
void chg(int x,int y){
// 之所以y-w[x]是因为题目中是“修改为”,此时加上差值就等于修改为
sg.chg(0,y-w[x],0,n,sg.rt[x]);
for(int i=x,d;fa[i];i=fa[i]){
d=dis[x][dep[x]-dep[fa[i]]];
sg.chg(d,y-w[x],0,n,sg.rt[fa[i]]);
ch.chg(d,y-w[x],0,n,ch.rt[i]);
}
}
}P;
int main(){
n=read(),m=read();
for(int i=1;i<=n;++i) w[i]=read();
for(int i=1,u,v;i<n;++i){
u=read(),v=read();
addedge(u,v),addedge(v,u);
}
P.init();
int lst=0;
while(m--){
int op=read(),x=read(),y=read();
x^=lst,y^=lst;
if(!op) write(lst=P.query(x,y));
else P.chg(x,y),w[x]=y;
}
return fw,0;
}
P3345 [ZJOI2015] 幻想乡战略游戏
题意:动态求一棵树的带点权、边权重心。
本题考察对点分树的理解。观察到如果当前点不是带权重心,那么一定有一个儿子会比它更优,查询时不断像这样跳儿子就行。又看到查询是动态的,于是考虑点分树。建好点分树之后我们可以暴力算出每一个点作为根时的答案,把儿子的答案和当前点的答案作比较,如果有儿子更优就往儿子跳即可。
于是问题转化为已知一个点求树上所有点到这个点的带权距离。具体实现就是套路了:设
ll sum[MAXN],smd[MAXN],smf[MAXN];
void getrt(int u,int fno){
siz[u]=1;
int s=0;
for(int i=head[u],v;i;i=e[i].to){
if((v=e[i].v)==fno||del[v]) continue;
getrt(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sm-siz[u]);
if(s<mxs) mxs=s,rt=u;
}
void build(int u){
del[u]=1;
for(int i=head[u],v;i;i=e[i].to){
if(del[v=e[i].v]) continue;
mxs=sm=siz[v];
getrt(v,u);
fa[rt]=u;
g[u].emplace_back(v,rt);
build(rt);
}
}
void mdf(int u,ll k){
sum[u]+=k;
for(int i=u;fa[i];i=fa[i]){
ll ds=L.getdis(u,fa[i]);
sum[fa[i]]+=k;
smd[fa[i]]+=k*ds;
smf[i]+=k*ds;
}
}
ll count(int u){
ll res=smd[u];
for(int i=u;fa[i];i=fa[i]){
ll ds=L.getdis(u,fa[i]);
res+=(sum[fa[i]]-sum[i])*ds;
res+=smd[fa[i]]-smf[i];
}
return res;
}
ll query(int u){
ll x=count(u);
for(auto vv:g[u])
if(count(vv.first)<x)
return query(vv.second);
return x;
}
int main(){
n=read(),Q=read();
for(int i=1,u,v,w;i<n;++i){
u=read(),v=read(),w=read();
addedge(u,v,w),addedge(v,u,w);
}
mxs=sm=n;
getrt(1,0);
int RT=rt;
build(rt);
L.dfs(1,0);
L.dfs2(1,1);
while(Q--){
int x=read(),y=read();
mdf(x,y);
write(query(RT));
}
return fw,0;
}
P3241 [HNOI2015] 开店
先考虑如果没有
那么加上
其实这个扩展是很巧妙的,利用了点分树的第二条性质。我们把原来的 +=
改成 emplace_back
一个点的点权信息和贡献信息。这样处理完毕之后,把每个点的 vector 按照点权排序,再把第二维前缀和一下,于是查询的时候只需用
这题有坑。
int mxs,sm,rt,siz[MAXN];
bool del[MAXN];
void getrt(int u,int fno){
int s=0;
siz[u]=1;
for(int i=head[u],v;i;i=e[i].to){
if((v=e[i].v)==fno||del[v]) continue;
getrt(v,u);
siz[u]+=siz[v];
s=max(s,siz[v]);
}
s=max(s,sm-siz[u]);
if(s<mxs) mxs=s,rt=u;
}
struct PIL{
int first;
ll second;
PIL(int u,int v):first(u),second(v){}
bool operator<(const PIL&x)const{
return first<x.first;
}
};
vector<PIL>smd[MAXN],smf[MAXN];
int fa[MAXN];
void dfs(int u,int fno,ll w){
smd[rt].emplace_back(x[u],w);
if(fa[rt]) smf[rt].emplace_back(x[u],L.getdis(u,fa[rt]));
for(int i=head[u],v;i;i=e[i].to){
if(del[v=e[i].v]||v==fno) continue;
dfs(v,u,w+e[i].w);
}
}
void build(int u){
del[u]=1;
dfs(u,0,0);
for(int i=head[u],v;i;i=e[i].to){
if(del[v=e[i].v]) continue;
mxs=sm=siz[v];
getrt(v,0);
getrt(rt,0);
fa[rt]=u;
build(rt);
}
}
ll query(int opt,int u,int l,int r,int&sz){
int L,R;
if(!opt){
L=lower_bound(smd[u].begin(),smd[u].end(),PIL(l,0ll))-smd[u].begin()-1;
R=upper_bound(smd[u].begin(),smd[u].end(),PIL(r,0ll))-smd[u].begin()-1;
}else{
L=lower_bound(smf[u].begin(),smf[u].end(),PIL(l,0ll))-smf[u].begin()-1;
R=upper_bound(smf[u].begin(),smf[u].end(),PIL(r,0ll))-smf[u].begin()-1;
}
// 这个L实际上是L-1所在位置,R就是R的位置
sz=R-L;
ll res=0;
if(!opt){
if(R>=0&&R<(int)smd[u].size()) res+=smd[u][R].second;
if(L>=0&&L<(int)smd[u].size()) res-=smd[u][L].second;
}else{
if(R>=0&&R<(int)smf[u].size()) res+=smf[u][R].second;
if(L>=0&&L<(int)smf[u].size()) res-=smf[u][L].second;
}
return res;
}
int main(){
n=read(),Q=read(),A=read();
for(int i=1;i<=n;++i) x[i]=read();
for(int i=1,u,v,w;i<n;++i){
u=read(),v=read(),w=read();
addedge(u,v,w),addedge(v,u,w);
}
L.dfs(1,0);
L.dfs2(1,1);
sm=mxs=n;
getrt(1,0);
getrt(rt,0);
build(rt);
for(int i=1;i<=n;++i){
sort(smd[i].begin(),smd[i].end());
sort(smf[i].begin(),smf[i].end());
if(smd[i].size()>1)
for(auto it=next(smd[i].begin());it!=smd[i].end();++it)
it->second+=prev(it)->second;
if(smf[i].size()>1)
for(auto it=next(smf[i].begin());it!=smf[i].end();++it)
it->second+=prev(it)->second;
}
ll lst=0;
while(Q--){
int u=read(),l=(read()+lst)%A,r=(read()+lst)%A,sz1,sz2;
if(l>r) swap(l,r);
lst=query(0,u,l,r,sz1);
for(int i=u;fa[i];i=fa[i]){
lst+=query(0,fa[i],l,r,sz2)-query(1,i,l,r,sz1);
lst+=(sz2-sz1)*L.getdis(fa[i],u);
}
write(lst);
}
return fw,0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!