点分治 模板 + 详解

动态点分治学了以后会在后面 update 的啦~

好久没颓 blog 了 今天来写一发

最近几个月就学了这一个东西啊 =-=

好了 进入正题 舌尖上的淀粉质

Q1:点分治是什么?

A1:就是像分治一样把树上的点咔擦成几个小树,然后继续咔擦下去处理问题啦 像你把西兰花掰开一样

Q2:点分治能用来干什么?

A2:据我所知,点分治通常是用来找树上所有路径和一个不可告人的数字之间的关系=-=例如小于k的路径啦,膜k余233的路径啦...

 

好了 按照套路 题目还是洛谷的模板 然后首先是

1.介绍变量

可能有点难看 但是放外面更难看=-=

2019.2.23 Update:好像 son 数组用不到 只要在 getrt 里面 int son = 0 就可以了=-=

struct edge{int ne,to,v;}e[N << 1]; //存无向边

int first[N],ans[10000005],siz[N],son[N];
//用于邻接表 存路径长度(桶) 子树大小 最大子树大小

int ed[N]; //记录经过当前点的所有分路径 用于累加 每次用直接覆盖不用清空

int n,m,size,mnsiz,rt,tot; //tot存边的时候用 然后记录路径的时候当成edg这个数组的指针
//size:当前树大小 mnsiz/rt:当前最小的 树的(子树大小/编号(也就是当前树的重心))

bool o[N]; //判断当前点有没有被divide(分治)过 求树的重心时用这个判断当前树的边界

2.建树

这里就不用我多讲了吧,能学到淀粉质的应该都会邻接表了

3.求树的重心(getrt)

这个部分网上很清楚了 这里随便讲讲

当前点 像树剖一样把子树 siz 弄出来 (不懂树剖也没关系不重要) 然后找个最大

再然后呢 树的完全体的点数为 n 当前点设为 p 则当前点 p 为根时 他到原根的那颗子树的 siz 就是 n - p

这个就是 p 点往上的那个子树啦 所以也要判断更新

当然这个存不了 只能每次判断 反正也花不了多少时间

每次分治之后都要求一次当前树的重心!这个要记得

下放代码

void getrt(int p,int fa) {
	siz[p] = 1,son[p] = 0;
	for (int a = first[p],b = e[a].to ; a ; a = e[a].ne,b = e[a].to)
		if (!o[b] && b != fa) {
		getrt(b,p);
		siz[p] += siz[b];
		if (son[p] < siz[b]) son[p] = siz[b];
	}
	if (size - siz[p] > son[p]) son[p] = size - siz[p];
	if (son[p] < mnsiz) mnsiz = son[p],rt = p;
}

4.分治(divide)

这个网上的那群毒瘤dalao们都不讲啊 讲完求树的重心就全放代码跑了=-=

其实也挺简单 就是求出当前重心后 对其所有儿子(当然不能让他父亲)分治

这里统计完当前点的答案后 要把当前点子树的答案分别算一遍并删去

原因?戳这里进去看看,我懒得画图了

对于 divide 我们先不思考如何 solve 更新答案 我们先把这个子程序搞定

怎么搞呢 遍历树边 然后分治子树 好了没了 exm?

然后分治子树之前 要重置与求树的重心有关的变量 我的这里面是这几个

mnsiz = INF,rt = 0,size = siz[b];

如果不知道为什么要赋值成这样的 我这里再搬一下变量

//size:当前树大小
//mnsiz/rt:当前最小的 树的(子树大小/编号(也就是当前树的重心))

INF是取极大值方便更新 这个应该不用我补充(那你补充干嘛)

话说为什么要重新求重心呢?因为如果你分治的子树的根又成了一条链的话....哼哼

放心 树已经通过判断的 o 数组分开了的 你只会搜到这棵子树的所有的点!

然后总的代码在这里 关于里面的 solve 下面立马会讲到

void divide(int p) {
	o[p] = 1;
	solve(p,0,1);
	for (int a = first[p],b = e[a].to ; a ; a = e[a].ne,b = e[a].to)
		if (!o[b]) {
		solve(b,e[a].v,0);
		mnsiz = INF,rt = 0,size = siz[b];
		getrt(b,0);
		divide(rt);
	}
}

5.统计答案(solve)

solve 就是处理答案啦 这里的 query 就是求经过当前点 p 的 终点为 p 的路径

然后记录的话就是之前说的 ed 啦 相当于一个桶 这东西用 tot 当作指针 所以要清零 tot

query 先放下来吧 也说不了什么 主要就是深搜下去的时候要把连接两点的边权加上

还有一开始的 dis 为 0 是为了记录经过 solve里面的 p 的 终点为 p 的路径 好吧其实不用说大家都懂 =-=

void query(int p,int fa,int dis) {
	ed[++tot] = dis;
	for (int a = first[p],b = e[a].to ; a ; a = e[a].ne,b = e[a].to)
		if (!o[b] && b != fa) query(b,p,dis + e[a].v);
}

solve 处理答案的时候 直接拼接即可 像冒泡排序一样的打法 然后删去的时候也是一样

因此就不用打两个子程序了 solve 里加个 mode 变量 即可 这里 1 为 记录路径 0 为 删去重路径

我写的这个双重循环在某点儿子很多的时候会跑得非常慢 像聪聪可可里面一个毒瘤数据 root 有10000+个儿子

题解里有优化,快得多 =w=

路径统计就是去掉重复的答案嘛 如果实在忘记为什么的话 看回 4 里面的那个 超链接 我搬下来了

下放代码

void solve(int rt,int dis,int mode) {
	tot = 0;
	query(rt,0,dis);
	if (mode) {
		for (int a = 1 ; a < tot ; ++ a)
		for (int b = a + 1 ; b <= tot ; ++ b) ++ans[ed[a] + ed[b]];
		return;
	}
	for (int a = 1 ; a < tot ; ++ a)
	for (int b = a + 1 ; b <= tot ; ++ b) --ans[ed[a] + ed[b]];
}

好了 以上就是全部 拼起来就可以了

下放总的代码 顺便把上面的模板链接拉下来

其实还是挺慢的 900+ms 吸氧 240+ms 别人有的都 10+ms

应该还有更好的方法 =-=

#include <cstdio>
#define N 10010
#define INF 0x3fffffff
inline int r() {
	int x = 0,y = 0; char q = getchar();
	while (q < '0' && q != '-' || q > '9') q = getchar();
	if (q == '-') ++ y,q = getchar();
	while ('0' <= q && q <= '9') x = x * 10 + q - 48,q = getchar();
	return y ? -x : x;
}
struct edge{int ne,to,v;}e[N << 1];
int first[N],ans[10000005],siz[N],son[N],ed[N];
int n,m,size,mnsiz,rt,tot;
bool o[N];
inline void add(int x,int y,int z) {
	e[++tot].ne = first[x],e[tot].to = y,e[tot].v = z,first[x] = tot;
	e[++tot].ne = first[y],e[tot].to = x,e[tot].v = z,first[y] = tot;
}
void getrt(int p,int fa) {
	siz[p] = 1,son[p] = 0;
	for (int a = first[p],b = e[a].to ; a ; a = e[a].ne,b = e[a].to)
		if (!o[b] && b != fa) {
		getrt(b,p);
		siz[p] += siz[b];
		if (siz[b] > son[p]) son[p] = siz[b];
	}
	if (size - siz[p] > son[p]) son[p] = size - siz[p];
	if (son[p] < mnsiz) mnsiz = son[p],rt = p;
}
void query(int p,int fa,int dis) {
	ed[++tot] = dis;
	for (int a = first[p],b = e[a].to ; a ; a = e[a].ne,b = e[a].to)
		if (!o[b] && b != fa) query(b,p,dis + e[a].v);
}
void solve(int rt,int dis,int mode) {
	tot = 0;
	query(rt,0,dis);
	if (mode) {
		for (int a = 1 ; a < tot ; ++ a)
		for (int b = a + 1 ; b <= tot ; ++ b) ++ans[ed[a] + ed[b]];
		return;
	}
	for (int a = 1 ; a < tot ; ++ a)
	for (int b = a + 1 ; b <= tot ; ++ b) --ans[ed[a] + ed[b]];
}
void divide(int p) {
	o[p] = 1;
	solve(p,0,1);
	for (int a = first[p],b = e[a].to ; a ; a = e[a].ne,b = e[a].to)
		if (!o[b]) {
		solve(b,e[a].v,0);
		mnsiz = INF,rt = 0,size = siz[b];
		getrt(b,0);
		divide(rt);
	}
}
int main() {
	n = r(),m = r();
	for (int a = 1,x,y,z ; a < n ; ++ a)
		x = r(),y = r(),z = r(),add(x,y,z);
	tot = 0,mnsiz = INF,size = n;
	getrt(1,0),divide(rt);
	while (m--) puts(ans[r()] ? "AYE" : "NAY");
	return 0;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值