[图论] 点分治
点分治
一些定义
-
点分治,可以类似成图上(树上)的分治算法
-
利用 分治+递归,对于某些树上的限定路径进行静态统计的算法
-
是处理树上路径的一个极好的方法。如果你需要大规模的处理一些树上路径的问题时,点分治是一个不错的选择。
-
分治的点的错误选择会导致时间复杂度十分不稳定,需要结合树的重心进行优化,否则特殊构造的图不知道会把你卡成什么样
P3806 【模板】点分治1
题意
给你一棵
题解
树的重心求法
-
定义:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡
-
当我们选择树的重心为分支点时,是最优的
void find_rt(int u,int fa){
sz[u]=1;mx[u]=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || vis[v])continue;//vis数组之后分治要用到
find_rt(v,u);
sz[u]+=sz[v];
Mx(mx[u],sz[v]);
}
Mx(mx[u],S-sz[u]);
if(mx[u]<mx[rt])rt=u;//取mx尽量小的
}
我们在点分治过程中每次选取子树的重心为子树的树根进行处理, 这样总的递归深度不会超过
例题题解
给定一棵有n个点的树。
询问树上距离为k的点对是否存在。
此
不难发现树上的路径分为两类:
-
经过根节点的路径
-
包含于子树的路径
对于前者,可以用
对于后者,递归处理(采用分治思想的体现)
点分治题的思想大都如上, 对于不同的题要设计不同的
本题中,
利用桶的思想,进行如下操作:
-
假设当前的根为
rt ,它的子树分别为v_1,v_2\cdots,v_i ,当前在子树v_i ,设judge[dis] 表示子树中v_1,v_2\cdots,v_{i-1} 到rt 的距离为dis 的点是否存在 -
然后我们通过
rem 数组类似于vector 当前子树v_i 中的点到rt 的距离 -
离线扫描每个询问,如果存在一组数据满足
judge[query[k]-rem[j]]=1 ,那么此回答为true , 可以用一个test 异或处理 -
基本思想就是上面说的:将当前子树
v_i 中的结点与前面的子树进行两两配对 -
将配对完后的
rem 数组一起保存进judge 数组,继续处理下一棵子树 -
当以
rt 为根的子树查询完后清空judge 数组对子树进行分治 -
由于调整了树的姿态,需要额外的
vis 数组记录统计过的rt
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int read()
{
int f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
inline void CheckMax(int &x,int y){if(x<y)x=y;return ;}
const int maxn = 1e5 + 10 , INF = 1e8 + 10;
int head[maxn],cnt=0;
struct edge{
int to,nxt,w;
}e[maxn<<1];
inline void link(int u,int v,int w){
e[++cnt].to=v;e[cnt].nxt=head[u];head[u]=cnt;e[cnt].w=w;
}
int sz[maxn],mx[maxn],rt,S;
bool vis[maxn];
void find_rt(int u,int fa){
sz[u]=1;mx[u]=0;//多测清空
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v] || v==fa)continue;
find_rt(v,u);
sz[u]+=sz[v];
CheckMax(mx[u],sz[v]);
}
CheckMax(mx[u],S-sz[u]);
if(mx[u]<mx[rt])rt=u;//rt需要初始化为INF
}
int n,m;
int query[maxn],dis[maxn],rem[maxn],test[INF],que[maxn];
bool judge[INF];
void getdis(int u,int fa){
rem[++rem[0]]=dis[u];
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa || vis[v])continue;
dis[v]=dis[u]+e[i].w;
getdis(v,u);
}
}
void calc(int u){//没必要记录fa,u的fa肯定被标记上了
int t=0;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
rem[0]=0;dis[v]=e[i].w;
getdis(v,u);//统计一棵子树
for(int j=rem[0];j>=1;j--)
for(int k=1;k<=m;k++)if(query[k]>=rem[j])test[k]|=judge[query[k]-rem[j]];
for(int j=rem[0];j>=1;j--)judge[rem[j]]=true,que[++t]=rem[j];
}
for(int i=1;i<=t;i++)judge[que[i]]=false;
}
void solve(int u){
vis[u]=true;judge[0]=true;//0肯定是可以满足的,否则在第一棵子树会wa
calc(u);
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
S=sz[v];mx[rt=0]=INF;
find_rt(v,0);solve(rt);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1,u,v,w;i<n;i++){
scanf("%d%d%d",&u,&v,&w);
link(u,v,w);link(v,u,w);
}
for(int i=1;i<=m;i++)query[i]=read();
mx[rt]=INF;S=n;
find_rt(1,0);
solve(rt);
for(int i=1;i<=m;i++){
if(test[i])puts("AYE");
else puts("NAY");
}
return 0;
}
注意桶开小会RE
几个细节
-
多次扫描清空,可以一边
dfs 一边清空,其中mx[rt] 需要弄成INF -
递归处理有时没有必要记录父亲结点,记录了有时反而简单问题复杂化
-
利用
vector 的思想保存数据有时非常必要