Logo xuyunao 的博客

博客

P3806 【模板】点分治 1 题解

...
xuyunao
2025-12-01 12:51:02
Dtw_ 可爱喵,KSCD_ 可爱喵

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2025-05-13 08:40:54

P3806 【模板】点分治 1 题解

什么是点分治

当一道树上题目可以离线,想要求得子树内路径上的某些信息时,就可以使用点分治。

来看这道题目。

题目描述

给定一棵有 $n$ 个点的树,询问树上距离为 $k$ 的点对是否存在。

  • 对于 $100\%$ 的数据,保证 $1 \leq n\leq 10^4$,$1 \leq m\leq 100$,$1 \leq k \leq 10^7$,$1 \leq u, v \leq n$,$1 \leq w \leq 10^4$。

做法

暴力做法非常简单,我们先把询问离线下来,然后用 $O(n^2)$ 去枚举两个点 $u,v$,利用树上差分求解,总时间复杂度 $O(n^2\log n)$。

我们考虑以 $u$ 为根的子树内路径都有哪些情况,发现只有两类:

  • 经过树的根 $u$,
  • 不经过树根,在某棵子树内

不难发现,对于第二种情况,我们可以在子树内某个点为树根时统计到它的贡献,因此我们只需要考虑如何计算第一种情况。

首先我们需要求出子树内每个点到根的距离,在这里使用 $d$ 数组表示,这样我们就可以使用两条路径拼成一条长的路径。

\/\/统计出每个点路径长度
int cnt;
int dis[maxn],d[maxn];
void getdep(int u,int fa)
{
	dis[++cnt] = d[u];
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(v == fa || vis[v]) continue;
		d[v] = d[u] + w;
		getdep(v,u);
	}
}

但是如果两个点 $u,v$ 在树根的同一棵子树内,那么就会产生前面的第二种情况,也就是与自己子树内的点计算贡献。因此我们考虑在计算完当前点与之前点的贡献后,再统一把这棵子树的距离值加进去。

我们使用一个 $dis$ 数组来记录当前树上已经遍历过的路径长度,那么我们每次计算完路径长度,就可以统计答案。

具体的,外层循环枚举路径长度,内层枚举每个询问。
我们令 $dis[i]$ 表示当前枚举的路径长度,$k$ 表示当前询问的长度,那么我们需要查询长度为 $k - dis[i]$ 的路径是否存在,使用一个桶记录一下就好。

int q[maxn];\/\/队列 存储当前子树内产生贡献的点 
bool judge[10000010];\/\/判断一个数是否存在 

void getans(int u)
{
	judge[0] = 1;\/\/长度为0的路径存在,目的是统计长度为k的路径
	int p = 0;\/\/队列长度
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(vis[v]) continue;
		cnt = 0;\/\/记录路径数量
		d[v] = w;
		getdep(v,u);
		for(int i = 1;i <= cnt;i++)
			for(int j = 1;j <= m;j++)
				if(que[j] >= dis[i]) ans[j] |= judge[que[j] - dis[i]];\/\/统计答案
				
		for(int i = 1;i <= cnt;i++)
		{
			if(dis[i] <= 1e7)
			{
				judge[dis[i]] = 1;
				q[++p] = dis[i];\/\/加入队列
			}
		}
	}
	for(int i = 1;i <= p;i++) judge[q[i]] = 0;\/\/全部处理完后要清空
}

要注意,当我们处理完所有子树后,对根的分治就完成了,此时为了避免当前子树内的路径对其它子树产生贡献,我们需要清空它的贡献。注意不要使用 memset,否则时间复杂度就假了。

由于我们需要递归的去做,所以复杂度是和树的深度与子树大小密切相关的,因此我们需要找出一种最优的方案,来减少递归。

在树型结构中,重心具有很多优秀的性质,删掉重心后,其他的子树都尽可能地保持平衡,最大子树大小最小。

因此我们对一棵子树,选择重心进行点分治,这样对于整棵树,只需要进行 $O(\log n)$ 次。

int siz[maxn],f[maxn];\/\/siz子树大小,f最大子树 
int vis[maxn];\/\/表示每个点是否已经被分治过 

\/\/求重心 
void getroot(int u,int fa)
{
	siz[u] = 1;
	f[u] = 0;
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(vis[v] || v == fa) continue;
		getroot(v,u);
		siz[u] += siz[v];
		f[u] = max(f[u],siz[v]);
	}
	f[u] = max(f[u],s - siz[u]);
	if(f[u] < f[rt]) rt = u;
}

到这里基本已经完成了,接下来只需要完善一下分治以及主函数即可。

我们发现点分治大致有四个步骤:

  • 求出当前子树重心
  • 求出子树内点到重心的距离
  • 统计答案、清空
  • 向下分治

到这里,我们已经做完了这道点分治模板题。

AC Code

#include<bits\/stdc++.h>

using namespace std;

const int inf = 0x3f3f3f3f;
const int maxn = 1e4 + 10;

struct note{
	int v,w;
};
vector<note> G[maxn];

int n,m;
int que[maxn];\/\/question 
int s,rt;\/\/存储当前子树大小及重心 
bool ans[maxn];\/\/存储问题答案 

int siz[maxn],f[maxn];\/\/siz子树大小,f最大子树 
int vis[maxn];\/\/表示每个点是否已经被分治过 

\/\/求重心 
void getroot(int u,int fa)
{
	siz[u] = 1;
	f[u] = 0;
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(vis[v] || v == fa) continue;
		getroot(v,u);
		siz[u] += siz[v];
		f[u] = max(f[u],siz[v]);
	}
	f[u] = max(f[u],s - siz[u]);
	if(f[u] < f[rt]) rt = u;
}

int cnt;
int dis[maxn],d[maxn];

\/\/统计出每个点路径长度 
void getdep(int u,int fa)
{
	dis[++cnt] = d[u];
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(v == fa || vis[v]) continue;
		d[v] = d[u] + w;
		getdep(v,u);
	}
}

int q[maxn];\/\/队列 存储当前子树内产生贡献的点 
bool judge[10000010];\/\/判断一个数是否存在 

void getans(int u)
{
	judge[0] = 1;
	int p = 0;
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(vis[v]) continue;
		cnt = 0;
		d[v] = w;
		getdep(v,u);
		for(int i = 1;i <= cnt;i++)
			for(int j = 1;j <= m;j++)
				if(que[j] >= dis[i]) ans[j] |= judge[que[j] - dis[i]];
				
		for(int i = 1;i <= cnt;i++)
		{
			if(dis[i] <= 1e7)
			{
				judge[dis[i]] = 1;
				q[++p] = dis[i];
			}
		}
	}
	for(int i = 1;i <= p;i++) judge[q[i]] = 0;
}

\/\/递归分治
void divide(int u)
{
	vis[u] = 1;\/\/标记当前点 
	getans(u);\/\/统计当前点的答案 
	for(auto nxt : G[u])
	{
		int v = nxt.v;
		int w = nxt.w;
		if(vis[v]) continue;
		s = siz[v];
		f[rt = 0] = inf;
		getroot(v,0);
		divide(rt);
	}
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for(int i = 1;i < n;i++)
	{
		int u,v,w;
		cin >> u >> v >> w;
		G[u].push_back({v,w});
		G[v].push_back({u,w});
	}
	for(int i = 1;i <= m;i++) cin >> que[i];
	f[rt = 0] = inf; \/\/初始化 
	s = n; \/\/当前子树点的数量 
	getroot(1,0);\/\/找到重心 
	getroot(rt,0);\/\/从重心进行一遍dfs,求出每个点子树大小等信息 
	divide(rt);\/\/分治 
	for(int i = 1;i <= m;i++) \/\/输出答案 
		cout << (ans[i] ? "AYE" : "NAY") << '\n';
	return 0;
}

需要注意的细节:

函数调用不要调用错误。
记得及时清空变量及数组,函数运行前初始化信息。

希望对你有所帮助。

评论

暂无评论

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。