浅谈点分治
非常有趣的一个知识点。
所谓点分治,也就是在树上进行分治的操作。
分治可以处理许多问题,各种区间类问题很多都可以利用分治思想,例如线段树就是利用分治处理许多区间性问题。
而目前处理树上区间问题学过的只有树剖,但树剖的缺陷性其实很大,当你需要对子树进行合并操作时,树剖会有很大的局限性,因为只有当整棵树有根时,树剖的节点才会有一定的有序性,而在树无根的情况下,就不好进行操作。
所以我们引入了一个新算法,点分治(淀粉质)。
点分治的基础思想基于分治,找到一个节点,使这个节点分开的子树的节点最大的尽量小。
如图中,
而
像节点
而由于分治不是只进行一次,所以我们对剩下来的子树继续分裂,下一次三个子树得到的重心为
分治分为两种操作,那就是分裂与合并,所以根据题目的实际情况进行分割即可。
首先我们先看下代码。
void divide(int x,int num)//x 为目前的重心,num 有关找重心操作。
{
vis[x]=1;
for(int i=h[x];i!=0;i=b[i].next)
{
int v=b[i].to;
if(vis[v])continue;
root=0;//即寻找的重心。
//以下操作有关找重心。
if(siz[v]>siz[x])tot=num-siz[x];
else tot=siz[v];
findzx(v,x);
//end
divide(root,tot);
}
}
那现在我们要做的就是找重心的操作了。
我们设
设
寻找最小的
void findzx(int x,int fa)
{
siz[x]=1;
mp[x]=0;
for(int i=h[x];i!=0;i=b[i].next)
{
int v=b[i].to;
if(vis[v]||v==fa)continue;
findzx(v,x);
siz[x]+=siz[v];
mp[x]=max(mp[x],siz[v]);
}
mp[x]=max(mp[x],tot-siz[x]);
if(mp[x]<mp[root])root=x;
}
以上就是点分治的所有理论知识了。
点分治难点不在理论,而是应用,接下来我们会讲解一些例题。
这题是名牌点分治,所以我们从点分治考虑此问题。
由于它要求是否存在一条路径长度为
详解一下。
假设现在我们需要将
那么看到
但是能路径
那么我们记录出来三棵子树合法的边长有
要知道它们可以拼凑出来的长度,就是一个典型的
这是个好方法,但如果用传统
我们需要对这个方法进行优化。
其实方法也很简单,因为数组间空隙很大,利用一个数组记录有哪些边长存在,而不是记录每个边长的是否存在即可。
程序中
复杂度:
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N=1e4+5;
const int M=105;
const int T=1e7+5;
struct node
{
int to,data;
};
vector<node>a[N];
int n,m,t[M],root,num=1e9,vis[N],siz[N],mp[N],tot,dis[N],judge[T],tmp[N],cnt,q[N],ans[N];
void dfs(int x,int fa)
{
int len=a[x].size();
siz[x]=1;
mp[x]=0;
for(int i=0;i<len;i++)
{
if(vis[a[x][i].to]||a[x][i].to==fa)continue;
dfs(a[x][i].to,x);
siz[x]+=siz[a[x][i].to];
mp[x]=max(mp[x],siz[a[x][i].to]);
}
mp[x]=max(mp[x],tot-siz[x]);
if(mp[x]<mp[root])root=x;
}
void get_dis(int x,int fa,int num)
{
if(num>1e7)return;
tmp[++cnt]=num;
int len=a[x].size();
for(int i=0;i<len;i++)
{
if(a[x][i].to==fa||vis[a[x][i].to])continue;
get_dis(a[x][i].to,x,num+a[x][i].data);
}
}
void clac(int x)
{
int len=a[x].size();
int cnp=0;
for(int i=0;i<len;i++)
{
if(vis[a[x][i].to])continue;
cnt=0;
dis[a[x][i].to]=a[x][i].data;
get_dis(a[x][i].to,x,a[x][i].data);
for(int j=1;j<=cnt;j++)for(int k=1;k<=m;k++)if(t[k]>=tmp[j])ans[k]|=judge[t[k]-tmp[j]];
for(int j=1;j<=cnt;j++)q[++cnp]=tmp[j],judge[tmp[j]]=1;
}
for(int i=1;i<=cnp;i++)judge[q[i]]=0;
}
void solve(int x)
{
vis[x]=1;
clac(x);
int len=a[x].size();
for(int i=0;i<len;i++)
{
if(vis[a[x][i].to])continue;
tot=siz[a[x][i].to];
root=0;
dfs(a[x][i].to,0);
solve(root);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
a[u].push_back({v,w});
a[v].push_back({u,w});
}
for(int i=1;i<=m;i++)scanf("%d",&t[i]);
tot=n;
root=0;
mp[0]=1e9;
judge[0]=1;
dfs(1,0);
solve(root);
for(int i=1;i<=m;i++)
{
if(ans[i])puts("AYE");
else puts("NAY");
}
return 0;
}
但是点分治也存在缺陷性,它很难去维护带修改的操作,每次修改的复杂度高达
点分树是点分治中分裂的点对分裂的子树的重心建起边的一棵树。
还是此图:
那么建的点分树即为:
这棵树的性质极多,如在原图中
但最重要的是,新的树深度接近于
这题提示我们使用点分树,那就用吧。
首先先建一棵点分树。
然后看一下我们需要的操作,再在点分树上进行操作。
第一个,我们需要查询
对于点分树上的每一个点,建一棵线段树,统计其他的点与他的距离。
这个复杂度我们肯定是承受不下的,所以得想办法利用点分树的性质去优化。
我们考虑维护在点分树内任意点
这个复杂度感觉上还是承受不下,但实际上是没有问题的,因为点分树最大的深度为
那么每次查询,从这个点
还有个问题,就是对于每一次统计,都会重复统计一些点,所以要进行排重。
一般的想法就是对那条路上的第一个子节点减去
那怎么办呢,实际上也很好弄,再维护一个线段树,下标代表以自己为线段树的节点与父亲的距离。
那么去重时只需去掉第二棵线段树以
两棵线段树的下标权值皆为节点的
复杂度:
这题得使用树剖求
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N=2e5+5;
int f[N],n,m,dep[N],tot,root,siz[N],mp[N],vis[N],a[N],t[N],k,nl,nr,h[N],son[N],topp[N];
struct node2
{
int from,to,next;
}b[N];
struct segmentree
{
int rt[N],cnt;
struct node
{
int ls,rs,data,cnt;
}f[N*50];
void update(int &x,int l,int r)
{
if(!x)x=++cnt;
if(l==r)
{
f[x].data+=k;
return;
}
int mid=(l+r)>>1;
if(mid>=nl)update(f[x].ls,l,mid);
else update(f[x].rs,mid+1,r);
f[x].data=f[f[x].ls].data+f[f[x].rs].data;
}
int search(int x,int l,int r)
{
if(!x)return 0;
if(l>=nl&&r<=nr)return f[x].data;
int mid=(l+r)>>1,num=0;
if(mid>=nl)num+=search(f[x].ls,l,mid);
if(mid<nr)num+=search(f[x].rs,mid+1,r);
return num;
}
}t1,t2;
int dfs(int x,int fa)
{
dep[x]=dep[fa]+1;
f[x]=fa;
int sum=1,maxn=-1;
for(int i=h[x];i!=0;i=b[i].next)
{
int v=b[i].to;
if(fa==v)continue;
int num=dfs(v,x);
if(num>maxn)maxn=num,son[x]=v;
sum+=num;
}
return sum;
}
void dfs2(int x,int tp)
{
topp[x]=tp;
if(son[x])dfs2(son[x],tp);
for(int i=h[x];i!=0;i=b[i].next)
{
int v=b[i].to;
if(v==f[x]||v==son[x])continue;
dfs2(v,v);
}
}
int lca(int x,int y)
{
while(topp[x]!=topp[y])
{
if(dep[topp[x]]<dep[topp[y]])swap(x,y);
x=f[topp[x]];
}
if(dep[x]>dep[y])swap(x,y);
return x;
}
void findzx(int x,int fa)
{
siz[x]=1;
mp[x]=0;
for(int i=h[x];i!=0;i=b[i].next)
{
int v=b[i].to;
if(vis[v]||v==fa)continue;
findzx(v,x);
siz[x]+=siz[v];
mp[x]=max(mp[x],siz[v]);
}
mp[x]=max(mp[x],tot-siz[x]);
if(mp[x]<mp[root])root=x;
}
void divide(int x,int num)
{
vis[x]=1;
for(int i=h[x];i!=0;i=b[i].next)
{
int v=b[i].to;
if(vis[v])continue;
root=0;
if(siz[v]>siz[x])tot=num-siz[x];
else tot=siz[v];
findzx(v,x);
a[root]=x;
divide(root,tot);
}
}
void ins(int x)
{
int cur=x;
while(cur)
{
if(a[cur])
{
nl=dep[a[cur]]+dep[x]-dep[lca(x,a[cur])]*2;
t2.update(t2.rt[cur],0,n-1);
}
nl=dep[x]+dep[cur]-dep[lca(x,cur)]*2;
t1.update(t1.rt[cur],0,n-1);
cur=a[cur];
}
}
int solve(int x,int kt)
{
int cur=x,bef=0,res=0;
while(cur)
{
int LCA=lca(x,cur);
if(kt-(dep[cur]+dep[x]-2*dep[LCA])<0)
{
bef=cur,cur=a[cur];
continue;
}
nl=0,nr=kt-(dep[cur]+dep[x]-2*dep[LCA]);
res+=t1.search(t1.rt[cur],0,n-1);
if(bef)nl=0,nr=kt-(dep[cur]+dep[x]-2*dep[LCA]),res-=t2.search(t2.rt[bef],0,n-1);
bef=cur;
cur=a[cur];
}
return res;
}
int read()
{
char ch=getchar();
int sum=0,f=1;
while(ch<'0'||ch>'9')
{
if(ch=='-')f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
sum=(sum<<1)+(sum<<3)+(ch^48);
ch=getchar();
}
return sum*f;
}
void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
if(x/10)write(x/10);
putchar((x%10)^48);
}
int main()
{
//freopen("P6329_1.in","r",stdin);
//freopen("a.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n;i++)t[i]=read();
int cmt=0;
for(int i=1;i<n;i++)
{
int u,v;
u=read();
v=read();
b[++cmt].from=u;
b[cmt].to=v;
b[cmt].next=h[u];
h[u]=cmt;
b[++cmt].from=v;
b[cmt].to=u;
b[cmt].next=h[v];
h[v]=cmt;
}
dfs(1,0);
dfs2(1,1);
root=0;
mp[0]=1e9;
tot=n;
findzx(1,0);
divide(root,n);
for(int i=1;i<=n;i++)k=t[i],ins(i);
int bef=0;
for(int i=1;i<=m;i++)
{
int opt,x,y;
opt=read(),x=read(),y=read();
x^=bef,y^=bef;
if(opt==0)
{
y=min(y,n-1);
int ans=solve(x,y);
write(ans);
putchar('\n');
bef=ans;
}
if(opt==1)k=y-t[x],ins(x),t[x]=y;
}
return 0;
}
练习题(别指望我能做多少):
P2634 [国家集训队]聪聪可可(比点分治板子还简单……)
P2664 树上游戏(练习题)
P3714 [BJOI2017]树的难题(上面的升级版)
P4149 [IOI2011]Race(变式板子)
P3241 [HNOI2015]开店(点分树)
P4075 [SDOI2016]模式字符串(非人做的点分治)
P4183 [USACO18JAN]Cow at Large P(好像不是很难)
P4292 [WC2010]重建计划(非人做的点分树)
P5306 [COCI2018-2019#5] Transport(不怎么难的点分治,练习题)
P2056 [ZJOI2007] 捉迷藏(目测一般的点分树)
P3345 [ZJOI2015]幻想乡战略游戏(目测一般点分树?)
P3920 [WC2014]紫荆花之恋(太经典了)
P4220 [WC2018]通道(???)
SP2666 QTREE4 - Query on a tree IV(疑似与捉迷藏一样)
P4565 [CTSC2018]暴力写挂(???????????????????)
我认输了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】