原文
LCA+桶+树上差分
1. 第一步
- 首先可以初步判断这个题肯定要计算LCA,方法有倍增/Tarjan-DFS,我们就写个简单的倍增吧,使用链式前向星存储边。
- 选择1号结点开始dfs,别的结点也可以
- dfs过程中计算
fa[][]
数组(fa[x][i]
表示 xx 结点的 2i2i 代祖先是谁)和deep[]数组(deep[x]
表示结点 xx 在树中的深度)
#include<bits/stdc++.h> using namespace std; const int SIZE=300000; int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE]; //w[i]表示i结点出现观察员的时间 struct edge { int to, next; }E[SIZE*2], e1[SIZE*2], e2[SIZE*2]; //边集数组e1,e2留待备用 void add(int x, int y) //加边函数 { E[++tot].to=y; E[tot].next=h[x]; h[x]=tot; } void dfs1(int x) //dfs的过程中完成“建树”,预处理fa[][]数组, 计算deep[]数组 { for(int i=1; (1<<i)<=deep[x]; i++) fa[x][i]=fa[fa[x][i-1]][i-1]; //x的2^i代祖宗就是x的2^{i-1}代祖宗的2^{i-1}代祖宗 for(int i=h[x]; i; i=E[i].next) { int y=E[i].to; if(y==fa[x][0]) continue; //如果y是父结点,跳过 fa[y][0]=x; deep[y]=deep[x]+1; dfs1(y); } } int get_lca(int x, int y) //计算x和y的最近公共祖先 { if(x==y) return x; //没有这一行,遇到 lca(x, x) 这样的询问时会挂掉 if(deep[x]<deep[y]) swap(x, y); //保持x的深度大于y的深度 int t=log(deep[x]-deep[y])/log(2); for(int i=t; i>=0; i--) //x向上跳到和y同样的深度 { if(deep[fa[x][i]]>=deep[y]) x=fa[x][i]; if(x==y) return x; } t=log(deep[x])/log(2); for(int i=t; i>=0; i--) //x和y一起向上跳 { if(fa[x][i]!=fa[y][i]) x=fa[x][i], y=fa[y][i]; } return fa[x][0]; } int main() //先把主函数写上一部分 { scanf("%d%d", &n, &m); for(int i=1; i<n; i++) { int u, v; scanf("%d%d", &u, &v); add(u, v); add(v, u); } deep[1]=1; fa[1][0]=1; dfs1(1); for(int i=1; i<=n; i++) scanf("%d", &w[i]); ///////////////////////////////////////////////////////////// ////////////////////////未完待续/////////////////////////// ///////////////////////////////////////////////////////////// return 0; }
2. 第二步
大概分析一下,m个玩家对应m条路径,有了起点和终点的 lca 后,如果我们模拟这个过程:
直觉
- 从起点 SiSi 跑到 LCALCA 在树长得很匀称的情况下为 O(lgn)O(lgn)
- 从起点 LCALCA 跑到 TiTi 在树长得很匀称的情况下为 O(lgn)O(lgn)
- 因此,模拟一个玩家的跑步过程为 O(lgn)O(lgn),m个玩家为 O(mlgn)O(mlgn)
- 理想情况下是可行的,但现实就是不理想
- 题目清楚告诉你,树会退化成一条链,因此模拟一个过程变成 O(n)O(n),总的就是。。。O(mn)O(mn),必挂无疑
- 此法不是正解!
尝试
- 我们能不能改变模拟跑步的过程,从 O(n)O(n) 优化到 O(lgn)O(lgn) 呢?思前想后不可能,有 nn 个观察员矗在那里,你可以对哪个视而不见?
- 路已走到尽头
转换
- 这时候需要放大招,转换思想!或许解决问题的思路压根就不是一个玩家一个玩家模拟,而是整体处理呢?
- 也就是说,我们不枚举每个运动员,而是枚举每个观察员i,看看哪些结点会为这个观察员i做贡献(刚好在wiwi秒跑到他这儿)。
- 枚举观察员的过程就是DFS整颗树的过程,我们可以在 O(n)O(n) 内搞定!
- 对于观察员i,哪些人会为他做贡献呢?
深入分析
- 对于结点 PP, 如果他位于一条起点、终点分别为 sisi, titi 的跑步路径上,如何判断这名选手会不会为 PP 作贡献呢?
- 分情况考虑
- 如果 PP 是在从 sisi 到 LCALCA 的路上,如下图:
-
我们可以得出结论:当起点 sisi 满足 deep[si]=w[P]+deep[P]deep[si]=w[P]+deep[P]时,起点 sisi会为 PP 观察员做一个贡献(运动员从sisi出发,可以被PP处的观察员在w[P]w[P]秒看到)
-
如果 PP 是在从 LCALCA 到 titi 的路上,如下图:
- 定义 dist[si,ti]dist[si,ti]为从 sisi出发到titi的路径长度,如果运动员从sisi出发,可以被PP处的观察员在w[P]w[P]秒观察到,可以由上图得出以下式子:
- dist[si,ti]−w[P]=deep[ti]−deep[P]dist[si,ti]−w[P]=deep[ti]−deep[P],移项后得到:
- dist[si,ti]−deep[ti]=w[P]−deep[P]dist[si,ti]−deep[ti]=w[P]−deep[P]
- 我们可以得出结论:当终点 titi 满足 dist[si,ti]−deep[ti]=w[P]−deep[P]dist[si,ti]−deep[ti]=w[P]−deep[P]时,终点 titi会为 PP 观察员做一个贡献
- 做一个重要的总结:上行过程中,满足条件的起点可以做贡献,下行过程中,满足条件的终点可以做贡献,但无论是哪一种情形,能对 PP 做贡献的起点或终点一定都在以PP为根的子树上,这使得可以在DFS回溯的过程中处理以任意节点为根的子树。
3. 第三步
如何统计子树贡献
- 递归以PP为根的子树时,可以统计出其子树中所有的起点和终点对它的贡献
- 这里又需要转换
- 子树中有的起点和终点对PP产生了贡献,有些不对其产生贡献但对PP以外的结点产生了贡献
- 所以我们不能枚举每个点(子树根),找子树中哪些点对其产生贡献,这样复杂度就上去了
- 而是对于树上的任何一个起点和终点,把其产生的贡献放在桶里面,回溯到子树根的时候再到桶里面查询结果
- 有人产生疑问了,也是很多人看不懂这里桶用法的地方,疑问如图:
- cc点产生贡献放在桶的deep[c]deep[c]位置,计算bb点获得的贡献时当然是从bucket1[deep[b]+w[b]]bucket1[deep[b]+w[b]]位置获取,于是得到1个贡献,你发现aa结点也是用的同一个桶,这个还好,因为cc确实给他做了贡献,可是ee点呢?他是不应该获得贡献的!既然我会给和我无关的结点做贡献,那么其它无关的结点难免也会给我做贡献!
- 问题总结一下,对于一个点PP来说,究竟哪些点在桶里面产生的贡献才是有效的。
- 答案是:以PP为根递归整颗子树过程中在桶内产生的差值才是有效的
还要考虑一种情况
- 先看图:
- 看懂了吗?对于以PP为根的内部路径(不经过PP),这条路径的起点和终点产生的贡献是不应该属于PP的
- 所以dfs过程中,在统计当前结点作为起点和终点所产生的贡献后,继而计算出当前结点作为“根”上的差值后,在回溯过程中,一定要减去以当前结点为LCALCA的起点、终点在桶里产生的贡献,这部分贡献在离开这个子树后就没有意义了。
代码说明
e1,tot1,h1,add1
是使用链式前向星的方法存储每个结点作为终点对应的路径集合e2,tot2,h2,add2
是使用链式前向星的方法存储每个结点作为LCA对应的路径集合b1,b2
是两组桶,分别用于上行阶段和下行阶段的贡献统计js[SIZE]
用于统计以每个结点作为起点的路径条数dist[SIZE], s[SIZE], t[SIZE]
用于统计m条路径对应的长度,起点和终点信息ans[SIZE]
存储最后输出的答案,是每个结点观察员看到的人数
int tot1, tot2, h1[SIZE], h2[SIZE]; void add1(int x, int y) { e1[++tot1].to=y; e1[tot1].next=h1[x]; h1[x]=tot1; } void add2(int x, int y) { e2[++tot2].to=y; e2[tot2].next=h2[x]; h2[x]=tot2; } int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], ans[SIZE]; void dfs2(int x) { int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE]; //递归前先读桶里的数值,t1是上行桶里的值,t2是下行桶的值 for(int i=h[x]; i; i=E[i].next) //递归子树 { int y=E[i].to; if(y==fa[x][0]) continue; dfs2(y); } b1[deep[x]]+=js[x]; //上行过程中,当前点作为路径起点产生贡献,入桶 for(int i=h1[x]; i; i=e1[i].next) //下行过程中,当前点作为路径终点产生贡献,入桶 { int y=e1[i].to; b2[dist[y]-deep[t[y]]+SIZE]++; } ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2; //计算上、下行桶内差值,累加到ans[x]里面 for(int i=h2[x]; i; i=e2[i].next) //回溯前清除以此结点为LCA的起点和终点在桶内产生的贡献,它们已经无效了 { int y=e2[i].to; b1[deep[s[y]]]--; //清除起点产生的贡献 b2[dist[y]-deep[t[y]]+SIZE]--; //清除终点产生的贡献 } } int main() { ////////////////重复部分跳过//////////// ////////////////文末提供完整代码//////// for(int i=1; i<=m; i++) //读入m条询问 { scanf("%d%d", &s[i], &t[i]); int lca=get_lca(s[i], t[i]); //求LCA dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]]; //计算路径长度 js[s[i]]++; //统计以s[i]为起点路径的条数,便于统计上行过程中该结点产生的贡献 add1(t[i], i); //第i条路径加入到以t[i]为终点的路径集合中 add2(lca, i); //把每条路径归到对应的LCA集合中 if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--; //见下面的解释 } dfs2(1); //dfs吧! for(int i=1; i<=n; i++) printf("%d ", ans[i]); return 0; }
一些重要补充
- 上述代码中有一行未加解释
if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
- 考虑路径是这样的,如图:
- 这个图可能不太好懂,意思是:
- 如果路径起点或终点刚好为LCA且LCA处是可观察到运动员的,那么我们在上行统计过程中和下行统计过程中都会对该LCA产生贡献,这样就重复计数一次!
-
好在这种情况很容易发现,我们提前预测到,对相应的结点进行
ans[x]--
即可。 -
此外,在使用第二个桶时,下标是
w[x]-deep[x]
会成为负数,所以使用第二个桶时,下标统一+SIZE
,向右平移一段区间,防止下溢。
4. 结束
完整代码
#include<bits/stdc++.h> using namespace std; const int N=300000; int n, m, tot, h[N], deep[N], fa[N][20], w[N];//w[i]表示i结点出现观察员的时间 struct edge{ int to, next; }E[N*2], e1[N*2], e2[N*2]; void add(int x, int y){ //加边 E[++tot].to=y; E[tot].next=h[x]; h[x]=tot; } int tot1, tot2, h1[N], h2[N]; void add1(int x, int y){ e1[++tot1].to=y; e1[tot1].next=h1[x]; h1[x]=tot1; } void add2(int x, int y){ e2[++tot2].to=y; e2[tot2].next=h2[x]; h2[x]=tot2; } //dfs的过程中完成“建树”,预处理fa[][]数组, 计算deep[]数组 void dfs1(int x){ for(int i=1; (1<<i)<=deep[x]; i++) fa[x][i]=fa[fa[x][i-1]][i-1]; //x的2^i代祖宗就是x的2^{i-1}代祖宗的2^{i-1}代祖宗 for(int i=h[x]; i; i=E[i].next){ int y=E[i].to; if(y==fa[x][0]) continue; //如果y是父结点,跳过 fa[y][0]=x; deep[y]=deep[x]+1; dfs1(y); } } int get_lca(int x, int y){ //计算x和y的最近公共祖先 if(x==y) return x; if(deep[x]<deep[y]) swap(x, y); //保持x的深度大于y的深度 int t=log(deep[x]-deep[y])/log(2); for(int i=t; i>=0; i--){ //x向上跳到和y同样的深度 if(deep[fa[x][i]]>=deep[y]) x=fa[x][i]; if(x==y) return x; } t=log(deep[x])/log(2); for(int i=t; i>=0; i--) //x和y一起向上跳 if(fa[x][i]!=fa[y][i]) x=fa[x][i], y=fa[y][i]; return fa[x][0]; } int b1[N*2], b2[N*2], js[N], dist[N], s[N], t[N], l[N], ans[N]; void dfs2(int x) { int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+N]; //递归前先读桶里的数值,t1是上行桶里的值,t2是下行桶的值 for(int i=h[x]; i; i=E[i].next) { //递归子树 int y=E[i].to; if(y==fa[x][0]) continue; dfs2(y); } b1[deep[x]]+=js[x];//上行过程中,当前点作为路径起点产生贡献,入桶 for(int i=h1[x]; i; i=e1[i].next) { //下行过程中,当前点作为路径终点产生贡献,入桶 int y=e1[i].to; b2[dist[y]-deep[t[y]]+N]++; } ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+N]-t2; //计算上、下行桶内差值,累加到ans[x]里面 for(int i=h2[x]; i; i=e2[i].next) { //回溯前清除以此结点为LCA的起点和终点在桶内产生的贡献,它们已经无效了 int y=e2[i].to; b1[deep[s[y]]]--; //清除起点产生的贡献 b2[dist[y]-deep[t[y]]+N]--;//清除终点产生的贡献 } } int main() { scanf("%d%d", &n, &m); for(int i=1; i<n; i++) { int u, v; scanf("%d%d", &u, &v); add(u, v); add(v, u); } deep[1]=1; fa[1][0]=1; dfs1(1); for(int i=1; i<=n; i++) scanf("%d", &w[i]); for(int i=1; i<=m; i++) { scanf("%d%d", &s[i], &t[i]); int lca=get_lca(s[i], t[i]); //求LCA dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]; //计算路径长度 js[s[i]]++; //统计以s[i]为起点路径的条数,便于统计上行过程中该结点产生的贡献 add1(t[i], i); //第i条路径加入到以t[i]为终点的路径集合中 add2(lca, i); //把每条路径归到对应的LCA集合中 if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--; /*如果路径起点或终点刚好为LCA且LCA处是可观察到运动员的, 那么我们在上行统计过程中和下行统计过程中都会对该LCA产生 贡献,这样就重复计数一次. 对相应的结点进行ans[x]--即可*/ } dfs2(1); for(int i=1; i<=n; i++) printf("%d ", ans[i]); return 0; }