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

世界树

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

题目大意

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

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

思路

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

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

虚树上的点有哪些

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

如何建虚树

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

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

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

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

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

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

如何求答案

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

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

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

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

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

那分界点的深度 z 就是这个:
disxdisfax+degx+degfax+12

然后如果有分界点的位置到两个占有点的距离都相同,我们就要看编号小的。
那如何看到的距离相同呢?
要满足这个:
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; }

__EOF__

本文作者あおいSakura
本文链接https://www.cnblogs.com/Sakura-TJH/p/YBT_JPDH_5-3-1.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   あおいSakura  阅读(69)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示