一些关于点分治-点分树的唠叨(详解)
由于某种来自东方的神秘力量(学长的压迫),鄙人不得不去填点分治的坑...
点分治
点分治算法主要思想是(在树上)分别统记包含某节点其子树对其造成的贡献,直接说并不是那么清楚,看例题:
洛谷P3806 【模板】点分治1
题目背景
感谢 hzwer 的点分治互测。
题目描述
给定一棵有 个点的树,询问树上距离为 的点对是否存在。
输入格式
第一行两个数 。
第 到第 行,每行三个整数 ,代表树上存在一条连接 和 边权为 的路径。
接下来 行,每行一个整数 ,代表一次询问。
输出格式
对于每次询问输出一行一个字符串代表答案,存在输出 AYE
,否则输出 NAY
。
样例输入 #1
2 1
1 2 2
2
样例输出 #1
AYE
数据规模与约定
- 对于 的数据,保证 。
- 对于 的数据,保证 , 。
- 对于 的数据,保证 ,,,,。
提示
- 本题不卡常。
- 如果您 #7 一直 RE/TLE,不妨看看 这个帖子。
-点分治
首先对点分治有一个非常重要的操作:求重心。一般的求法有两种:1.从树根开始递归,每次向儿子及其子树大小之和大于 总树的大小的儿子递归。2.递归求取每一个点单独作为根的最大子树,选取最大子树最小的点。第一种求出的重心是错误的虽然复杂度是正确的,在此贴一篇大佬关于两种方法的比较:一种基于错误的寻找重心方法的点分治的复杂度分析。
求到了重心并将重心作为当前树的根向下递归,我们就能保证每次递归至少能够将子树划分一半,这也是点分治复杂度正确性的保证。
此题求长度为k的路径是否存在,对于以当前重心为根的树中,我们可以将路径划分两类:一类为经过当前重心的路径,一类为未经过重心的路径。第二类暂且不管,第一类的求法:
既然是求经过重心的路径,我们可以开一个桶存某一长度的路径在已经走过的子树中是否存在,对于当前子树,当走到某一个点且此点和根距离为时,我们可以查找桶中是否存在一条长度为的路径,即之前的子树中是否存在一条路径和当前的路径拼起来长度刚好为,满足条件“经过当前树的根”。
而对于第二类路径,向下递归(找重心、划分子树、求值)的过程是一定能够将其包含在内的。
点分治的分治过程即如上:找重心、划分子树、计算贡献、下一层... 而这也是点分治名称的由来,即以重心划分子树分治。
对于不同的点分治,大概计算贡献的过程不同吧...
代码如下:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
inline int read() {
int f=1,j=0;
char w=getchar();
while(w>'9'||w<'0') {
if(w=='-')f=-1;
w=getchar();
}
while(w>='0'&&w<='9') {
j=(j<<3)+(j<<1)+w-'0';
w=getchar();
}
return f*j;
}
const int N=10001;
int head[N],to[N*2],front[N*2],val[N*2],tail;
int n,m,k[N];
int size[N],maxn[N],last,root,mid[N],mid_last;
bool walk[N],ans[101];
bitset<100000001>Q;
inline void addline(int x,int y,int sumn) {
to[++tail]=y;
val[tail]=sumn;
front[tail]=head[x];
head[x]=tail;
return ;
};
void getcore(int nown,int fa,int all_size) {
size[nown]=1;
maxn[nown]=0;
for(int a=head[nown]; a; a=front[a]) {
int x=to[a];
if(x==fa||walk[x])continue;
getcore(x,nown,all_size);
size[nown]+=size[x];
maxn[nown]=max(maxn[nown],size[x]);
}
maxn[nown]=max(maxn[nown],all_size-size[nown]);
if(maxn[nown]<maxn[root])root=nown;
return ;
}
void getsize(int nown,int fa) {
size[nown]=1;
for(int a=head[nown]; a; a=front[a]) {
int x=to[a];
if(x==fa||walk[x])continue;
getsize(x,nown);
size[nown]+=size[x];
}
return ;
}
void getline(int nown,int fa,int sumn,int op=0) {
if(sumn<=10000000) {
mid[++mid_last]=sumn;
if(op)goto b;
for(int i=1; i<=m; i++) {
if(ans[i])continue;
if((k[i]>=sumn&&Q[k[i]-sumn])||sumn==k[i]) {
ans[i]=true;
}
}
}
b:;
for(int a=head[nown]; a; a=front[a]) {
int x=to[a];
if(x==fa||walk[x])continue;
getline(x,nown,sumn+val[a],op);
}
return ;
}
void work(int nown,int all_size) {
root=0;
maxn[root]=INT_MAX;
getcore(nown,0,all_size);
int core=root;
getsize(core,0);
walk[core]=true;
for(int a=head[core]; a; a=front[a]) {
int x=to[a];
if(walk[x])continue;
work(x,size[x]);
}
walk[core]=false;
for(int a=head[core]; a; a=front[a]) {
int x=to[a];
if(walk[x])continue;
mid_last=0;
getline(x,core,val[a]);
for(int i=1; i<=mid_last; i++)Q[mid[i]]=1;
}
for(int a=head[core]; a; a=front[a]) {
int x=to[a];
if(walk[x])continue;
mid_last=0;
getline(x,core,val[a],1);
for(int i=1; i<=mid_last; i++)Q[mid[i]]=0;
}
return ;
}
signed main() {
n=read();
m=read();
for(int i=1; i<=n-1; i++) {
int x=read(),y=read(),z=read();
addline(x,y,z);
addline(y,x,z);
}
for(int i=1; i<=m; i++)k[i]=read();
work(1,n);
for(int i=1; i<=m; i++) {
if(ans[i])printf("AYE\n");
else printf("NAY\n");
}
return 0;
}
点分树
点分树,即将每一层的重心相连构成一棵新树,如此可避免普通点分治每递归一次都要求重心且树严格平衡(达到查找一次的优秀复杂度)(加上一个树什么的就两只log罢)。
但是如此也会存在一些问题: 1.原树的结构在点分树上被完全打乱,此处需LCA求两点距离。 2.由于算点分树上当前点的父亲子树上的点对当前点的贡献可能会有一部分和当前点的子树对当前点的贡献重合,所以此处需维护两个值:A.当前点的子树对当前点的贡献 B.当前点的子树对当前点在点分树上的父亲的贡献。如此只需沿点分树逐层上爬沿途算贡献即可。
在此提供两种求LCA的方法:1.倍增求LCA,相信大多数人都会在普及组涉及吧。2.ST表+dfn序求LCA,求出原树dfn序的序列后,求两点的LCA即在dfn的序列上求两点构成区间中离根最近的点(深度最小)即为两者的LCA(显然正确)。第二种在询问多的情况下显然是优于第一种,而如果想要复杂度进一步优化,我们还可以采取ST表分块:每为一整块,块内部使用ST表。
(不得不说这样写的代码过于毒瘤,调了整整一上午...)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探