啊,我爱数学!|

Fun_Strawberry

园龄:10个月粉丝:1关注:0

点分治 学习笔记

前言

int08 于 2024 年 5 月 30 日才通过点分治模板,望周知。

点分治 学习笔记!

过了一会之后,我的肩膀开始有些疼了,我的 osu! 瘾也犯了,所以我先暂时把这个当做第一集,各位如果想看第二集,可以等待我打完 osu! 以及对点分治有更深的理解之后。

解决问题:点分治适合处理大规模的树上路径信息问题。

复杂度:模板 O(nlogn)

此处应有我自己对于“大规模的树上路径信息问题”的理解。

“树上”:点分治是树上问题的算法。

“路径”:就像分治处理点对一样,点分治处理树上点对,而树上点对自然涉及到它们之间的路径。

“信息”:信息通常与路径相关。(如果信息与异或相关,则一般不考虑点分治,因为路径异或可以通过求前缀和转为点权异或)

“大规模”:这是重点内容,我分两个方向理解,东北方一个,东方一个。

  1. 大规模表示不是处理具体给定的路径的问题,比如 P3384 【模板】重链剖分/树链剖分这样的问题并不适合点分治。不过这一条不绝对。

大规模更偏向于:

  • 询问存不存在一种路径

  • 找到所有路径中某种特征值最大/小的。

  • 符合条件的路径计数

这种和 O(n2) 条路径都相关的问题。

  1. 大规模不一定是要答案全局统计,也有可能是对于每个点分别统计相关点对答案。

因为点分治的每一轮,其实都是遍历了每个点的(复杂度只有一个 log),所以可以分开统计。

算法流程

把普通的分治套在树上,点分治的流程就显然应该是下面这样:

  1. 考虑每个子树的点与其它子树里的点之间的贡献(包括与根的贡献)。

  2. 递归进入子树考虑问题。

现在如果按照普通的分治逻辑思考就会出现三个问题:

  1. 普通分治只分成两块,所以可以确定左边一个,右边一个,这里会分成子树数量个,枚举两点属于的子树就会被菊花图卡成 O(n2)

  2. 普通分治中并没有单独考虑根这一环节。

  3. 碰到链这种结构,子树始终只有 1 的,递归次数达到 O(n2) 又要被卡回去。

我们依次解决。

  1. 第一个点枚举具体属于哪个子树是需要的,而枚举第二个点属于的子树并没有意义,我们只需要知道它不在原子树中即可。

如果枚举无序点对相关:钦定一个枚举顺序,把已经枚举过的部分扔到一起处理,如扔进桶,树状数组,Trie 树中。

如果枚举有序点对:如以某个点为起点一类,考虑将所有元素扔一个结构中,枚举某个子树答案时候从这个结构中删除,但是从两个方向分别枚举一般更加方便。

  1. 这就很简单了,在枚举开始之前就加上根的影响,如果寻找有序点对,根拉出来单独计算。

  2. 这也是点分治的核心问题。我们考虑每次选取树的重心为根,因为树的重心最大子树不超过 n2,只能递归 logn 层,点分治的复杂度便得到了保证。

说起来简单,写起来还是有一些难度的

具体实现流程

这里以 P3806 【模板】点分治 1 为例简述实现过程。

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

n104,k107,q100

流程咕咕咕。

Talk is cheap,show me the code.

Upd on 2024.7.16:我回来做点分树的时候吃了不熟悉点分治的亏花了很久才过,为了防止将来的我忘掉点分治,现在我决定加上一些注释。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 11451
int vis[N];    //很重要,点分治作为重心之后就标记,接下来就不再去。
bool tong[N*N];
int k;
int n,q;
vector<pair<int,int> > e[N];
int sz[N],mxs[N],rt;    //这里的 size 和 mxs 对应第一遍 dfs 求重心,rt 就是重心,注意它全局变量的特殊性。
vector<int> col[N];    //将每个子树对应的深度集合放在一起,便于计算
void dfs1(int u,int fa,int n)    //求重心,注意不要用二分之 n 求,有神秘错误。
{
	sz[u]=1;mxs[u]=0;
	for(auto v:e[u]) if(vis[v.first]==0&&v.first!=fa) dfs1(v.first,u,n),sz[u]+=sz[v.first],mxs[u]=max(sz[v.first],mxs[u]);
	mxs[u]=max(mxs[u],n-sz[u]);
	if(mxs[u]<=mxs[rt]) rt=u;
}
void dfs2(int u,int d,int co,int fa)    //只用于分好很多深度集合。
{
	col[co].push_back(d);
	for(auto v:e[u]) if(v.first!=fa&&vis[v.first]==0) dfs2(v.first,d+v.second,co,u);
}
bool solve(int r,int n)    //r 不是 点分治的重心,只是表示计算这个连通块
{
	if(n==1) return 0;
	rt=0;mxs[0]=998244353; //由于 mxs 和 rt 是全局变量,需要重置
	dfs1(r,0,n);
	int co=0;
	for(auto v:e[rt]) if(!vis[v.first]) dfs2(v.first,v.second,++co,rt);
	tong[0]=1;
	bool ans=0;
	for(int i=1;i<=co;i++)  //具体计算的过程,一个一个遍历集合,加入桶,和之前的桶内元素比较。
	{
		for(auto x:col[i]) if(x<=k&&tong[k-x]) ans=1;
		for(auto x:col[i]) tong[x]=1;    //先比后加
	}
	for(int i=1;i<=co;i++)
	{
		for(auto x:col[i]) tong[x]=0;
		col[i].clear();  //清空,消除影响。
	}
	if(ans) return ans;  //只是剪枝,计数时候要去掉。
	vis[rt]=1;
	for(auto v:e[rt]) if(!vis[v.first])
	{
		if(sz[v.first]<sz[rt]) ans|=solve(v.first,sz[v.first]);
		else ans|=solve(v.first,n-sz[rt]);    //这里分讨以传入正确的大小
		if(ans) return ans;
	}
	return 0;
}
int main()
{
	cin>>n>>q;
	int u,v,w;
	for(int i=1;i<n;i++) cin>>u>>v>>w,e[u].push_back({v,w}),e[v].push_back({u,w});
	while(q--)
	{
		cin>>k;
		cout<<(solve(1,n)?"AYE":"NAY")<<endl;
		memset(vis,0,sizeof(vis));
	}
	return 0;
}

本文作者:Fun_Strawberry's blog

本文链接:https://www.cnblogs.com/FunStrawberry/p/18223825

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Fun_Strawberry  阅读(16)  评论(0编辑  收藏  举报
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示