世界树
题目链接:ybt金牌导航5-3-1 / luogu P3233
题目大意
有一棵树,边的权值都是 1。然后有一些特殊点,对于每个点,它会被离它距离最近的特殊点占有。
然后不同的时刻特殊点也会不同,问你在每个询问,每个特殊点会各占有多少个点。
(可以自己占有自己)
思路
你首先看到它只要特殊点的值,然后你细看一下,我们在意的是两个相邻特殊点之间的信息,因为这样才会要判断中间的点属于哪个。
就比如说两个相邻特殊点的路径链,必然会要找一个分界点,使得两边的点分别被两个特殊点占有。
那我们就可以把树简化,把没有必要的点和边缩在一起。
那你看看怎么会有必要。首先,特殊点肯定是有必要的。
然后如果有这样的图:
那你会知道,如果一个点有两个子树有特殊点,它也要留下。不然你就会使得你树的形态改变。
那就会变成这个:
这个树,就是虚树。
虚树上的点,我们叫它关键点。
虚树上的点有哪些
那你会发现,如果一个点是关键点而不是特殊点,那它就是其中两个特殊点的 LCA。
然后显然,对于 dfs 序连续的的三个点 \(x,y,z\),有 \(\text{LCA}(x,z)=\text{LCA}(x,y)\) 或 \(\text{LCA}(y,z)\)。
那我们就只要把特殊点按 dfs 序排序之后,所有的 \(\text{LCA}(x_i,x_{i+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,fa_x\),点 \(i\) 到最近特殊点的距离是 \(dis_i\),深度是 \(deg_i\)。
那分界点的深度 \(z\) 就是这个:
\(\dfrac{dis_x-dis_{fa_x}+deg_x+deg_{fa_x}+1}{2}\)
然后如果有分界点的位置到两个占有点的距离都相同,我们就要看编号小的。
那如何看到的距离相同呢?
要满足这个:
\(dis_{fa_x}+z-deg_{fa_x}=dis_x+deg_x-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;
}