【ybt金牌导航5-3-1】【luogu P3233】世界树 / 虚树例题

37 篇文章 0 订阅
3 篇文章 0 订阅

世界树

题目链接:ybt金牌导航5-3-1 / luogu P3233

题目大意

有一棵树,边的权值都是 1。然后有一些特殊点,对于每个点,它会被离它距离最近的特殊点占有。

然后不同的时刻特殊点也会不同,问你在每个询问,每个特殊点会各占有多少个点。
(可以自己占有自己)

思路

你首先看到它只要特殊点的值,然后你细看一下,我们在意的是两个相邻特殊点之间的信息,因为这样才会要判断中间的点属于哪个。
就比如说两个相邻特殊点的路径链,必然会要找一个分界点,使得两边的点分别被两个特殊点占有。

那我们就可以把树简化,把没有必要的点和边缩在一起。
那你看看怎么会有必要。首先,特殊点肯定是有必要的。
然后如果有这样的图:
在这里插入图片描述
那你会知道,如果一个点有两个子树有特殊点,它也要留下。不然你就会使得你树的形态改变。
那就会变成这个:
在这里插入图片描述
这个树,就是虚树。
虚树上的点,我们叫它关键点。

虚树上的点有哪些

那你会发现,如果一个点是关键点而不是特殊点,那它就是其中两个特殊点的 LCA。
然后显然,对于 dfs 序连续的的三个点 x , y , z x,y,z x,y,z,有 LCA ( x , z ) = LCA ( x , y ) \text{LCA}(x,z)=\text{LCA}(x,y) LCA(x,z)=LCA(x,y) LCA ( y , z ) \text{LCA}(y,z) LCA(y,z)
那我们就只要把特殊点按 dfs 序排序之后,所有的 LCA ( x i , x i + 1 ) \text{LCA}(x_i,x_{i+1}) LCA(xi,xi+1) 就是那些点。
再加上特殊点,就是所有虚树的点。

如何建虚树

然后我们考虑怎么建图。
我们可以按着 dfs 序枚举每个特殊点,因为是按着 dfs 序,那我们可以一直维护最右的链。
然后插入一个点 x x x 的时候,如果这个最右的链的末端节点不是 x x x 的祖先(这个可以通过 LCA 来看),就把末端节点从最右的链中去掉,然后继续看。

然后你要维护连边,那当你删掉一个之后现在的末端节点的深度小于了 LCA,那你就把你刚刚删去的点和 LCA 连一条边。(因为这里只用维护父亲,你就是它的父亲标记成 LCA)

然后弄好之后,如果 LCA 和你当前点不一样,那就把它设是虚树上的点,然后 LCA 的父亲就是你当前点。当然,因为你是按着 dfs 序,所以 LCA 一定是在最右链,就把这个点放进去。

然后你再把 x x x 点放进最右链里面。然后它的父亲是 LCA。

那你可以在这个过程中,你每次找到新的虚树上的点。那些点就是上面说用 LCA 求的点。

然后后面还要用 dfs 序搞,那我们就把所有的虚树点按 dfs 序排序。
(它的 dfs 序就是原来树上的顺序,所以不用再跑一次图求,大小关系是一样的)

如何求答案

当然,你在一开始读入的时候就可以通过一次 dfs 跑图求出原来树的各种信息。
比如倍增要的父亲,子树大小,dfs 序,深度。

然后我们来看如何求答案。
首先,你要找最短距离,那我们就可以用 dp 来搞。
因为树上的路径可以表式为从一个点往上跳一定高度,再向下走一定高度。那我们就可以先自下而上 DP,然后自上而下 DP。

然后我们来看虚树上每个点和它的父亲的控制范围。
如果它没有父亲,那它会占有所有点,那所有点有哪些呢?
就是全部点减去它子树的大小。
每个虚树上的点和不是虚树上的子树会被同一个点控制。那有多少个呢?可以用整个树的大小减去在虚树上子树的大小。

如果它和它的父亲都被同一个点占有,那它们之间的点也肯定是被这个点占有。

然后如果两个点分别被不同的点占有呢?
那就会有一个分界点在两个点之间的路上,使得两边各属于不用的点。那是多少呢?
我们设两个点是 x , f a x x,fa_x x,fax,点 i i i 到最近特殊点的距离是 d i s i dis_i disi,深度是 d e g i deg_i degi

那分界点的深度 z z z 就是这个:
d i s x − d i s f a x + d e g x + d e g f a x + 1 2 \dfrac{dis_x-dis_{fa_x}+deg_x+deg_{fa_x}+1}{2} 2disxdisfax+degx+degfax+1

然后如果有分界点的位置到两个占有点的距离都相同,我们就要看编号小的。
那如何看到的距离相同呢?
要满足这个:
d i s f a x + z − d e g f a x = d i s x + d e g x − z dis_{fa_x}+z-deg_{fa_x}=dis_x+deg_x-z disfax+zdegfax=disx+degxz
至于为什么,你想想,画个图看看就知道了。

然后这样就好了。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#define INF 0x3f3f3f3f3f3f3f3f

using namespace std;

struct node {
	int to, nxt;
}e[600001];
int n, le[300001], KK;
int x, y, q, m, fa[300001][21];
int size[300001], deg[300001];
int dfn[300001], tmp, h[300001];
int p[300001], ans[300001], dis[300001];
pair <int, int> f[300001];
int sta[300001], fath[300001];
int val[300001], num;

bool cmp(int x, int y) {
	return dfn[x] < dfn[y];
}

void add(int x, int y) {
	e[++KK] = (node){y, le[x]}; le[x] = KK;
}

void dfs(int now, int father) {//dfs跑出一些值
	dfn[now] = ++tmp;//dfs序
	deg[now] = deg[father] + 1;//深度
	size[now] = 1;//子树大小
	for (int i = le[now]; i; i = e[i].nxt)
		if (e[i].to != father) {
			fa[e[i].to][0] = now;//父亲
			dfs(e[i].to, now);
			size[now] += size[e[i].to];
		}
}

void get_fath() {//倍增父亲
	for (int i = 1; i <= 20; i++)
		for (int j = 1; j <= n; j++)
			fa[j][i] = fa[fa[j][i - 1]][i - 1];
}

int LCA(int x, int y) {//LCA模板
	if (deg[x] < deg[y]) swap(x, y);
	for (int i = 20; i >= 0; i--)
		if (deg[fa[x][i]] >= deg[y])
			x = fa[x][i];
	if (x == y) return x;
	for (int i = 20; i >= 0; i--)
		if (fa[x][i] != fa[y][i]) {
			x = fa[x][i];
			y = fa[y][i];
		}
	return fa[x][0];
}

void get_tree() {//得到虚树
	sta[0] = 0;
	
	num = m;
	sort(p + 1, p + num + 1, cmp);
	
	for (int i = 1; i <= m; i++) {
		int now = p[i];
		
		if (!sta[0]) {
			sta[++sta[0]] = now;
			fath[now] = 0;
		}
		else {
			int lca = LCA(now, sta[sta[0]]);
			while (deg[lca] < deg[sta[sta[0]]]) {
				if (deg[sta[sta[0] - 1]] <= deg[lca])
					fath[sta[sta[0]]] = lca; 
				sta[0]--;
			}
			
			if (sta[sta[0]] != lca) {
				fath[lca] = sta[sta[0]];
				f[lca] = make_pair(INF, 0);
				sta[++sta[0]] = lca;
				p[++num] = lca;
			}
			
			fath[now] = lca;
			sta[++sta[0]] = now;
		}
	}
	
	sort(p + 1, p + num + 1, cmp);
}

int jump(int x, int y) {//求出一个点向上跳 x 级的儿子
	for (int i = 0; i <= 20; i++) {//利用倍增来跳
		if (y & 1) x = fa[x][i];
		y >>= 1;
		if (!y) return x;
	}
	return x;
}

void work() {
	for (int i = num; i >= 2; i--) {//从下到上 DP
		int now = p[i];
		int father = fath[now];
		
		dis[now] = deg[now] - deg[father];//算出两个点之间的链有多大
		
		if (f[father].first > f[now].first + dis[now] || (f[father].first == f[now].first + dis[now] && f[father].second > f[now].second)) {
			f[father].first = f[now].first + dis[now];
			f[father].second = f[now].second;
		}
	}
	for (int i = 2; i <= num; i++) {//从上到下 DP
		int now = p[i];
		int father = fath[now];
		
		if (f[now].first > f[father].first + dis[now] || (f[now].first == f[father].first + dis[now] && f[now].second > f[father].second)) {
			f[now].first = f[father].first + dis[now];
			f[now].second = f[father].second;
		}
	}
	for (int i = 1; i <= num; i++) {
		int now = p[i];
		int father = fath[now];
		val[now] = size[now];
		
		if (i == 1) {//没有父亲
			ans[f[now].second] += n - size[now];
			continue;
		}
		
		int son = jump(now, dis[now] - 1);//求出子树的根节点
		val[father] -= size[son];//求出不在虚树上的点
		int sum = size[son] - size[now];
		if (f[now].second == f[father].second) ans[f[now].second] += sum;//两个点都被同一个点占有
			else {
				int mid = (f[now].first - f[father].first + deg[now] + deg[father] + 1) >> 1;
				//算出分界点
				if (f[father].second < f[now].second && f[father].first + mid - deg[father] == deg[now] - mid + f[now].first)
					mid++;//距离相同,要给编号小的
				int mid_num = size[jump(now, deg[now] - mid)] - size[now];
				//算出下面占有点的个数
				ans[f[now].second] += mid_num;
				ans[f[father].second] += sum - mid_num;
				//其它点就是被上面占有点占有
			}
	}
	for (int i = 1; i <= num; i++)//加不在虚树上的点所提供的贡献
		ans[f[p[i]].second] += val[p[i]];
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i < n; i++) {
		scanf("%d %d", &x, &y);
		add(x, y);
		add(y, x);
	}
	
	dfs(1, 0);
	get_fath();
	
	scanf("%d", &q);
	for (int times = 1; times <= q; times++) {
		memset(ans, 0, sizeof(ans));
		
		scanf("%d", &m);
		for (int i = 1; i <= m; i++) {
			scanf("%d", &h[i]);
			p[i] = h[i];
			f[h[i]] = make_pair(0, h[i]);
		}
		
		get_tree();
		
		work();
		
		for (int i = 1; i <= m; i++)
			printf("%d ", ans[h[i]]);
		printf("\n");
	}
	
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值