[图论] 点分治

点分治

一些定义

  • 点分治,可以类似成图上(树上)的分治算法

  • 利用 分治+递归,对于某些树上的限定路径进行静态统计的算法

  • 是处理树上路径的一个极好的方法。如果你需要大规模的处理一些树上路径的问题时,点分治是一个不错的选择。

  • 分治的点的错误选择会导致时间复杂度十分不稳定,需要结合树的重心进行优化,否则特殊构造的图不知道会把你卡成什么样

P3806 【模板】点分治1

题意

给你一棵 \(n\) 个点的无根树,每条边上都有一个权值,求长度不超过 \(k\) 的路径有多少条(距离为\(k\) 的路径是否存在)。

题解

树的重心求法

  • 定义:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,删去重心后,生成的多棵树尽可能平衡

  • 当我们选择树的重心为分支点时,是最优的

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尽量小的
}

我们在点分治过程中每次选取子树的重心为子树的树根进行处理, 这样总的递归深度不会超过 \(logN\) , 整个点分治的时间复杂度也就保证了\(O(NlogN)\)


例题题解

给定一棵有n个点的树。

询问树上距离为k的点对是否存在。

\(.md\) 提供蓝书思想:

不难发现树上的路径分为两类:

  1. 经过根节点的路径

  2. 包含于子树的路径

对于前者,可以用 \(dis[u]\) 表示 \(u\)\(root\) 的距离,\(u\)\(v\) 的路径长即为 \(dis[u]+dis[v]\)

对于后者,递归处理(采用分治思想的体现

点分治题的思想大都如上, 对于不同的题要设计不同\(calc\) 函数处理问题。


本题中,\(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\) 的思想保存数据有时非常必要

posted @ 2021-08-12 17:11  ¶凉笙  阅读(37)  评论(0编辑  收藏  举报