世界树
题目链接: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} 2disx−disfax+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+z−degfax=disx+degx−z
至于为什么,你想想,画个图看看就知道了。
然后这样就好了。
代码
#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;
}