首先声明这不是一篇算法独特的题解,仍然是“LCA+桶+树上差分”,但这篇题解是为了让很多很多看了很多题解仍然看不懂的朋友们看懂的,其中就包括我,我也在努力地把解题的“思维过程”呈现出来,希望能帮助到别人。实在是佩服那些考场AC的大牛,再次向你们献上敬意!

1. 第一步
  • 首先可以初步判断这个题肯定要计算LCA,方法有倍增/Tarjan-DFS,我们就写个简单的倍增吧,使用链式前向星存储边。
  • 选择1号结点开始dfs,别的结点也可以
  • dfs过程中计算fa[][]数组(fa[x][i]表示 \(x\) 结点的 \(2^i\) 代祖先是谁)和deep[]数组(deep[x]表示结点 \(x\) 在树中的深度)
  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int SIZE=300000;
  4. int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE]; //w[i]表示i结点出现观察员的时间
  5. struct edge
  6. {
  7. int to, next;
  8. }E[SIZE*2], e1[SIZE*2], e2[SIZE*2]; //边集数组e1,e2留待备用
  9. void add(int x, int y) //加边函数
  10. {
  11. E[++tot].to=y;
  12. E[tot].next=h[x];
  13. h[x]=tot;
  14. }
  15. void dfs1(int x) //dfs的过程中完成“建树”,预处理fa[][]数组, 计算deep[]数组
  16. {
  17. for(int i=1; (1<<i)<=deep[x]; i++)
  18. fa[x][i]=fa[fa[x][i-1]][i-1]; //x的2^i代祖宗就是x的2^{i-1}代祖宗的2^{i-1}代祖宗
  19. for(int i=h[x]; i; i=E[i].next)
  20. {
  21. int y=E[i].to;
  22. if(y==fa[x][0]) continue; //如果y是父结点,跳过
  23. fa[y][0]=x;
  24. deep[y]=deep[x]+1;
  25. dfs1(y);
  26. }
  27. }
  28. int get_lca(int x, int y) //计算x和y的最近公共祖先
  29. {
  30. if(x==y) return x; //没有这一行,遇到 lca(x, x) 这样的询问时会挂掉
  31. if(deep[x]<deep[y]) swap(x, y); //保持x的深度大于y的深度
  32. int t=log(deep[x]-deep[y])/log(2);
  33. for(int i=t; i>=0; i--) //x向上跳到和y同样的深度
  34. {
  35. if(deep[fa[x][i]]>=deep[y])
  36. x=fa[x][i];
  37. if(x==y)
  38. return x;
  39. }
  40. t=log(deep[x])/log(2);
  41. for(int i=t; i>=0; i--) //x和y一起向上跳
  42. {
  43. if(fa[x][i]!=fa[y][i])
  44. x=fa[x][i], y=fa[y][i];
  45. }
  46. return fa[x][0];
  47. }
  48. int main() //先把主函数写上一部分
  49. {
  50. scanf("%d%d", &n, &m);
  51. for(int i=1; i<n; i++)
  52. {
  53. int u, v;
  54. scanf("%d%d", &u, &v);
  55. add(u, v);
  56. add(v, u);
  57. }
  58. deep[1]=1;
  59. fa[1][0]=1;
  60. dfs1(1);
  61. for(int i=1; i<=n; i++) scanf("%d", &w[i]);
  62. /////////////////////////////////////////////////////////////
  63. ////////////////////////未完待续///////////////////////////
  64. /////////////////////////////////////////////////////////////
  65. return 0;
  66. }
2. 第二步

大概分析一下,m个玩家对应m条路径,有了起点和终点的 lca 后,如果我们模拟这个过程:

直觉

  • 从起点 \(S_i\) 跑到 \(LCA\) 在树长得很匀称的情况下为 \(O(lgn)\)
  • 从起点 \(LCA\) 跑到 \(T_i\) 在树长得很匀称的情况下为 \(O(lgn)\)
  • 因此,模拟一个玩家的跑步过程为 \(O(lgn)\),m个玩家为 \(O(mlgn)\)
  • 理想情况下是可行的,但现实就是不理想
  • 题目清楚告诉你,树会退化成一条链,因此模拟一个过程变成 \(O(n)\),总的就是。。。\(O(mn)\),必挂无疑
  • 此法不是正解!

尝试

  • 我们能不能改变模拟跑步的过程,从 \(O(n)\) 优化到 \(O(lgn)\) 呢?思前想后不可能,有 \(n\) 个观察员矗在那里,你可以对哪个视而不见?
  • 路已走到尽头

转换

  • 这时候需要放大招,转换思想!或许解决问题的思路压根就不是一个玩家一个玩家模拟,而是整体处理呢?
  • 也就是说,我们不枚举每个运动员而是枚举每个观察员i,看看哪些结点会为这个观察员i做贡献(刚好在\(w_i\)秒跑到他这儿)。
  • 枚举观察员的过程就是DFS整颗树的过程,我们可以在 \(O(n)\) 内搞定!
  • 对于观察员i,哪些人会为他做贡献呢?

深入分析

  • 对于结点 \(P\), 如果他位于一条起点、终点分别为 \(s_i\), \(t_i\) 的跑步路径上,如何判断这名选手会不会为 \(P\) 作贡献呢?
  • 分情况考虑
  • 如果 \(P\) 是在从 \(s_i\) 到 \(LCA\) 的路上,如下图:

  • 我们可以得出结论:当起点 $ s_i $ 满足 $ deep[s_i]=w[P]+deep[P] $时,起点 \(s_i\)会为 \(P\) 观察员做一个贡献(运动员从\(s_i\)出发,可以被\(P\)处的观察员在\(w[P]\)秒看到)

  • 如果 \(P\) 是在从 \(LCA\) 到 \(t_i\) 的路上,如下图:

  • 定义 \(dist[s_i, t_i]\)为从 \(s_i\)出发到\(t_i\)的路径长度,如果运动员从\(s_i\)出发,可以被\(P\)处的观察员在\(w[P]\)秒观察到,可以由上图得出以下式子:
  • \(dist[s_i, t_i]-w[P]=deep[t_i]-deep[P]\),移项后得到:
  • $ dist[s_i, t_i]-deep[t_i]=w[P]-deep[P] $
  • 我们可以得出结论:当终点 $ t_i $ 满足 $ dist[s_i, t_i]-deep[t_i]=w[P]-deep[P] $时,终点 \(t_i\)会为 \(P\) 观察员做一个贡献
  • 做一个重要的总结:上行过程中,满足条件的起点可以做贡献,下行过程中,满足条件的终点可以做贡献,但无论是哪一种情形,能对 \(P\) 做贡献的起点或终点一定都在以\(P\)为根的子树上,这使得可以在DFS回溯的过程中处理以任意节点为根的子树。
3. 第三步

如何统计子树贡献

  • 递归以\(P\)为根的子树时,可以统计出其子树中所有的起点和终点对它的贡献
  • 这里又需要转换
  • 子树中有的起点和终点对\(P\)产生了贡献,有些不对其产生贡献但对\(P\)以外的结点产生了贡献
  • 所以我们不能枚举每个点(子树根),找子树中哪些点对其产生贡献,这样复杂度就上去了
  • 而是对于树上的任何一个起点和终点,把其产生的贡献放在桶里面,回溯到子树根的时候再到桶里面查询结果
  • 有人产生疑问了,也是很多人看不懂这里桶用法的地方,疑问如图:

  • \(c\)点产生贡献放在桶的\(deep[c]\)位置,计算\(b\)点获得的贡献时当然是从\(bucket1[deep[b]+w[b]]\)位置获取,于是得到1个贡献,你发现\(a\)结点也是用的同一个桶,这个还好,因为\(c\)确实给他做了贡献,可是\(e\)点呢?他是不应该获得贡献的!既然我会给和我无关的结点做贡献,那么其它无关的结点难免也会给我做贡献!
  • 问题总结一下,对于一个点\(P\)来说,究竟哪些点在桶里面产生的贡献才是有效的。
  • 答案是:以\(P\)为根递归整颗子树过程中在桶内产生的差值才是有效的

还要考虑一种情况

  • 先看图:

  • 看懂了吗?对于以\(P\)为根的内部路径(不经过\(P\)),这条路径的起点和终点产生的贡献是不应该属于\(P\)的
  • 所以dfs过程中,在统计当前结点作为起点和终点所产生的贡献后,继而计算出当前结点作为“根”上的差值后,在回溯过程中,一定要减去以当前结点为\(LCA\)的起点、终点在桶里产生的贡献,这部分贡献在离开这个子树后就没有意义了。

代码说明

  • e1,tot1,h1,add1是使用链式前向星的方法存储每个结点作为终点对应的路径集合
  • e2,tot2,h2,add2是使用链式前向星的方法存储每个结点作为LCA对应的路径集合
  • b1,b2是两组桶,分别用于上行阶段下行阶段的贡献统计
  • js[SIZE]用于统计以每个结点作为起点的路径条数
  • dist[SIZE], s[SIZE], t[SIZE]用于统计m条路径对应的长度,起点和终点信息
  • ans[SIZE]存储最后输出的答案,是每个结点观察员看到的人数
  1. int tot1, tot2, h1[SIZE], h2[SIZE];
  2. void add1(int x, int y)
  3. {
  4. e1[++tot1].to=y;
  5. e1[tot1].next=h1[x];
  6. h1[x]=tot1;
  7. }
  8. void add2(int x, int y)
  9. {
  10. e2[++tot2].to=y;
  11. e2[tot2].next=h2[x];
  12. h2[x]=tot2;
  13. }
  14. int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], ans[SIZE];
  15. void dfs2(int x)
  16. {
  17. int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE]; //递归前先读桶里的数值,t1是上行桶里的值,t2是下行桶的值
  18. for(int i=h[x]; i; i=E[i].next) //递归子树
  19. {
  20. int y=E[i].to;
  21. if(y==fa[x][0]) continue;
  22. dfs2(y);
  23. }
  24. b1[deep[x]]+=js[x]; //上行过程中,当前点作为路径起点产生贡献,入桶
  25. for(int i=h1[x]; i; i=e1[i].next) //下行过程中,当前点作为路径终点产生贡献,入桶
  26. {
  27. int y=e1[i].to;
  28. b2[dist[y]-deep[t[y]]+SIZE]++;
  29. }
  30. ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2; //计算上、下行桶内差值,累加到ans[x]里面
  31. for(int i=h2[x]; i; i=e2[i].next) //回溯前清除以此结点为LCA的起点和终点在桶内产生的贡献,它们已经无效了
  32. {
  33. int y=e2[i].to;
  34. b1[deep[s[y]]]--; //清除起点产生的贡献
  35. b2[dist[y]-deep[t[y]]+SIZE]--; //清除终点产生的贡献
  36. }
  37. }
  38. int main()
  39. {
  40. ////////////////重复部分跳过////////////
  41. ////////////////文末提供完整代码////////
  42. for(int i=1; i<=m; i++) //读入m条询问
  43. {
  44. scanf("%d%d", &s[i], &t[i]);
  45. int lca=get_lca(s[i], t[i]); //求LCA
  46. dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]]; //计算路径长度
  47. js[s[i]]++; //统计以s[i]为起点路径的条数,便于统计上行过程中该结点产生的贡献
  48. add1(t[i], i); //第i条路径加入到以t[i]为终点的路径集合中
  49. add2(lca, i); //把每条路径归到对应的LCA集合中
  50. if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--; //见下面的解释
  51. }
  52. dfs2(1); //dfs吧!
  53. for(int i=1; i<=n; i++) printf("%d ", ans[i]);
  54. return 0;
  55. }

一些重要补充

  • 上述代码中有一行未加解释if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
  • 考虑路径是这样的,如图:

  • 这个图可能不太好懂,意思是:

  • 如果路径起点或终点刚好为LCA且LCA处是可观察到运动员的,那么我们在上行统计过程中和下行统计过程中都会对该LCA产生贡献,这样就重复计数一次!

  • 好在这种情况很容易发现,我们提前预测到,对相应的结点进行ans[x]--即可。

  • 此外,在使用第二个桶时,下标是w[x]-deep[x]会成为负数,所以使用第二个桶时,下标统一+SIZE,向右平移一段区间,防止下溢。

4. 结束

我不知道自己说清楚没有,但愿大家不要拍砖头!下面是完整代码

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int SIZE=300000;
  4. int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];
  5. struct edge
  6. {
  7. int to, next;
  8. }E[SIZE*2], e1[SIZE*2], e2[SIZE*2];
  9. void add(int x, int y)
  10. {
  11. E[++tot].to=y;
  12. E[tot].next=h[x];
  13. h[x]=tot;
  14. }
  15. int tot1, tot2, h1[SIZE], h2[SIZE];
  16. void add1(int x, int y)
  17. {
  18. e1[++tot1].to=y;
  19. e1[tot1].next=h1[x];
  20. h1[x]=tot1;
  21. }
  22. void add2(int x, int y)
  23. {
  24. e2[++tot2].to=y;
  25. e2[tot2].next=h2[x];
  26. h2[x]=tot2;
  27. }
  28. void dfs1(int x)
  29. {
  30. for(int i=1; (1<<i)<=deep[x]; i++)
  31. fa[x][i]=fa[fa[x][i-1]][i-1];
  32. for(int i=h[x]; i; i=E[i].next)
  33. {
  34. int y=E[i].to;
  35. if(y==fa[x][0]) continue;
  36. fa[y][0]=x;
  37. deep[y]=deep[x]+1;
  38. dfs1(y);
  39. }
  40. }
  41. int get_lca(int x, int y)
  42. {
  43. if(x==y) return x;
  44. if(deep[x]<deep[y]) swap(x, y);
  45. int t=log(deep[x]-deep[y])/log(2);
  46. for(int i=t; i>=0; i--)
  47. {
  48. if(deep[fa[x][i]]>=deep[y])
  49. x=fa[x][i];
  50. if(x==y)
  51. return x;
  52. }
  53. t=log(deep[x])/log(2);
  54. for(int i=t; i>=0; i--)
  55. {
  56. if(fa[x][i]!=fa[y][i])
  57. x=fa[x][i], y=fa[y][i];
  58. }
  59. return fa[x][0];
  60. }
  61. int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], l[SIZE], ans[SIZE];
  62. void dfs2(int x)
  63. {
  64. int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];
  65. for(int i=h[x]; i; i=E[i].next)
  66. {
  67. int y=E[i].to;
  68. if(y==fa[x][0]) continue;
  69. dfs2(y);
  70. }
  71. b1[deep[x]]+=js[x];
  72. for(int i=h1[x]; i; i=e1[i].next)
  73. {
  74. int y=e1[i].to;
  75. b2[dist[y]-deep[t[y]]+SIZE]++;
  76. }
  77. ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;
  78. for(int i=h2[x]; i; i=e2[i].next)
  79. {
  80. int y=e2[i].to;
  81. b1[deep[s[y]]]--;
  82. b2[dist[y]-deep[t[y]]+SIZE]--;
  83. }
  84. }
  85. int main()
  86. {
  87. scanf("%d%d", &n, &m);
  88. for(int i=1; i<n; i++)
  89. {
  90. int u, v;
  91. scanf("%d%d", &u, &v);
  92. add(u, v);
  93. add(v, u);
  94. }
  95. deep[1]=1;
  96. fa[1][0]=1;
  97. dfs1(1);
  98. for(int i=1; i<=n; i++) scanf("%d", &w[i]);
  99. for(int i=1; i<=m; i++)
  100. {
  101. scanf("%d%d", &s[i], &t[i]);
  102. int lca=get_lca(s[i], t[i]);
  103. dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca];
  104. js[s[i]]++;
  105. add1(t[i], i);
  106. add2(lca, i);
  107. if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
  108. }
  109. dfs2(1);
  110. for(int i=1; i<=n; i++) printf("%d ", ans[i]);
  111. return 0;
  112. }

NOIP2016(D1T2)天天爱跑步题解的更多相关文章

  1. 「NOIP2016」天天爱跑步 题解

    (声明:图片来源于网络) 「NOIP2016」天天爱跑步 题解 题目TP门 题目 题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是 ...

  2. 【NOIP2016】天天爱跑步 题解(LCA+桶+树上差分)

    题目链接 题目大意:给定一颗含有$n$个结点的树,每个结点有一个权值$w$.给定$m$条路径,如果一个点与路径的起点的距离恰好为$w$,那么$ans[i]++$.求所有结点的ans. 题目分析 暴力的 ...

  3. LOJ #2359. 「NOIP2016」天天爱跑步(倍增+线段树合并)

    题意 LOJ #2359. 「NOIP2016」天天爱跑步 题解 考虑把一个玩家的路径 \((x, y)\) 拆成两条,一条是 \(x\) 到 \(lca\) ( \(x, y\) 最近公共祖先) 的 ...

  4. UOJ261 【NOIP2016】天天爱跑步 LCA+动态开点线段树

    UOJ261 [NOIP2016]天天爱跑步 Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.天天爱跑步是一个养成类游戏,需要玩家每天按时上线, ...

  5. NOIP2016天天爱跑步 题解报告【lca+树上统计(桶)】

    题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.«天天爱跑步»是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏的地图可以看作一一棵包含 nn个 ...

  6. [NOIP2016]天天爱跑步 题解(树上差分) (码长短跑的快)

    Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要 玩家每天按时上线,完成打卡任务.这个游戏的地图 ...

  7. 【NOIP2016】天天爱跑步

    题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.«天天爱跑步»是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏的地图可以看作一一棵包含 个结点 ...

  8. Noip 2016 天天爱跑步 题解

    [NOIP2016]天天爱跑步 时间限制:2 s   内存限制:512 MB [题目描述] 小C同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是 ...

  9. UOJ261 【NOIP2016】天天爱跑步

    本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000作者博客:http://www.cnblogs.com/ljh2000-jump/转 ...

随机推荐

  1. 七牛云 qshell 使用

    七牛云 qshell 控制台工具上传 命令:qshell fput another1 demo.txt /users/tianyang/demo.txt ======================= ...

  2. 微信小程序原生开发简介

    简介: 总结: 1. 逻辑层使用js引擎,视图层使用webview渲染 2. 微信小程序已经支持了绝大部分的 ES6 API 3. 可以自动补全css的兼容语法 文档:https://develope ...

  3. python学习日记(文件操作练习题)

    登录注册(三次机会) name = input('请注册姓名:') password = input('请注册密码:') with open('log',mode='w',encoding='utf- ...

  4. python学习日记(函数基础)

    修改文件(原理)--回顾 #修改文件(原理) with open('name','r',encoding='utf-8') as f,\ open('password','w+',encoding=' ...

  5. 洛谷P4689 [Ynoi2016]这是我自己的发明(莫队,树的dfn序,map,容斥原理)

    洛谷题目传送门 具体思路看别的题解吧.这里只提两个可能对常数和代码长度有优化的处理方法. I 把一个询问拆成\(9\)个甚至\(16\)个莫队询问实在是有点珂怕. 发现询问的一边要么是一个区间,要么是 ...

  6. 「SCOI2016」美味 解题报告

    「SCOI2016」美味 状态极差无比,一个锤子题目而已 考虑每次对\(b\)和\(d\)求\(c=d \ xor \ (a+b)\)的最大值,因为异或每一位是独立的,所以我们可以尝试按位贪心. 如果 ...

  7. Arukas.io云主机安装CentOS

    创建应用   1 jdeathe/centos-ssh:centos-6 启动应用 电机启动应用,应用会自动部署,等显示Running 就说明成功了.估计需要几分钟. 查看用户以及密码 自己保存下用户 ...

  8. luogu3188/bzoj1190 梦幻岛宝珠 (分层背包dp)

    他都告诉你能拆了 那就拆呗.把每个重量拆成$a*2^b$的形式 然后对于每个不同的b,先分开做30个背包 再设f[i][j]表示b<=i的物品中 容量为$ j*2^i+W\&((1< ...

  9. HEOI2016解题报告

    树 在2016年,佳媛姐姐刚刚学习了树,非常开心.现在他想解决这样一个问题:给定一颗有根树(根为1),有以下 两种操作:1. 标记操作:对某个结点打上标记(在最开始,只有结点1有标记,其他结点均无标记 ...

  10. centos7下kafka集群安装部署

    应用摘要: Apache kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写.Kafka是一种高吞吐量的 分布式发布订阅消息系统,是消息中间件的一种,用于构建实时 ...