洛谷题库 P3304 [SDOI2013]直径 题解与思路重现

洛谷题库 P3304 [SDOI2013]直径

想看正解请直接跳过文章前半段。

不知道到底错没错的错解

这是一道树的直径的必经边问题。

单单求树的直径是非常简单的。可以通过两次DFS/BFS或一次树形DP完成O(N)\mathrm{O}(N)的求解。显然,树的直径长度唯一,但构成该长度的链的方案可能不唯一。

一开始,我想通过两次树形DP完成求解:假设我们已求出直径长为DD。设f[x]{mx,mxcnt}f[x]\{mx, mxcnt\}记录在11号节点作根时节点xx的子树上的最大深度及其计数(深度最大的链可能不止一条),显然有f[x]={f[soni]f[soni].mx>f[x].mx{f[x].mx,f[x].mxcnt+f[soni].mxcnt}f[soni].mx=f[x].mxf[x] = \begin{cases}f[son_i] & f[son_i].mx > f[x].mx \\ \{f[x].mx, f[x].mxcnt + f[son_i].mxcnt\} & f[son_i].mx = f[x].mx \end{cases}进而通过换根DP求出F[x]{mx,mxcnt}F[x]\{mx, mxcnt\}即在以xx为整棵树的根时的最大深度和计数(求解方法类似于f[x]f[x])。求解F[x]F[x]时,xx的父亲节点fafaF[fa]F[fa]应当已经求出,这时如果边xfax \leftrightarrow fa在直径上,则有f[x].mx+F[fa].mx=Df[x].mx + F[fa].mx = D,且通过该边的直径条数应该是cnt=f[x].mxcnt×F[x].mxcntcnt = f[x].mxcnt \times F[x].mxcnt。如果它是一条必经边,则有cntmaxcntcnt \geq maxcnt,其中maxcntmaxcnt是全局中cntcnt的最大值。若有cntmaxcntcnt \ge maxcnt,则说明cntcnt才是直径条数”ansans重置为11;若有cnt=maxcntcnt = maxcnt,则ansans自增11

乍看,这个方法好像很有道理。正准备实现的时候,再一细想却傻眼了:

  1. 如果F[fa]F[fa]在转移时采取的是包含xx的方案(即以fafa作根时,最大深度的那条链恰好包含现在扫描到的xx),怎么办?
  2. 在菊花图上使用该方法,您将顺利得到所有的边都是直径必经边的错误结果。

对于第一个问题,我曾考虑过记录次大的F[x]F[x]f[x]f[x],转移时保证采取的F[x]F[x]不包含当前所在链的方案。但是实现起来太太太复杂了……还非常容易写错。至于为什么会出现第二个问题……请看下面的菊花图。

一个根节点度为3的菊花图

显然,该图共有3条直径2142 \leftrightarrow 1 \leftrightarrow 42132 \leftrightarrow 1 \leftrightarrow 33143 \leftrightarrow 1 \leftrightarrow 4,且必经边条数为00。采取如上方法计算,会得到cnt2=cnt3=cnt4=maxcnt=3cnt_2 = cnt_3 = cnt_4 = maxcnt = 3,导致三条边全部计入答案。

所以这个花了我两个晚自习加一个小时的方法报废了。

正解

在说明正解之前,需要捣鼓几条推论。

引理 1

若树的直径方案不唯一,则设一条直径的点集为VaV_a,另一条直径的点集为VbV_b,有VaVbV_a \cap V_b \neq \emptyset

采用反证法证明。假设有VaVb=V_a \cap V_b = \emptyset。设两条直径两端点分别为xa1,xa2x_{a1}, x_{a2}xb1,xb2x_{b1}, x_{b2},则由树上两点之间有且仅有一条简单路径可知,必然存在路径xa3xb3 (xa3Va,xb3Vb)x_{a3} \longleftrightarrow x_{b3} \space (x_{a3} \in V_a, x_{b3} \in V_b),使得两直径上任意两点相互可达。在两条直径上,xa1,xa2x_{a1}, x_{a2}xa3x_{a3}的距离、xb1,xb2x_{b1}, x_{b2}xb3x_{b3}的距离分别记作dis(xa1,xa3)\mathrm{dis}(x_{a1}, x_{a3})dis(xa2,xa3)\mathrm{dis}(x_{a2}, x_{a3})dis(xb1,xb3)\mathrm{dis}(x_{b1}, x_{b3})dis(xb2,xb3)\mathrm{dis}(x_{b2}, x_{b3})。容易知道,有max(dis(xa1,xa3),dis(xa2,xa3))D/2\max(\mathrm{dis}(x_{a1}, x_{a3}), \mathrm{dis}(x_{a2}, x_{a3})) \geq D/2max(dis(xb1,xb3),dis(xb2,xb3))D/2\max(\mathrm{dis}(x_{b1}, x_{b3}), \mathrm{dis}(x_{b2}, x_{b3})) \geq D/2。则取max(dis(xa1,xa3),dis(xa2,xa3))+max(dis(xb1,xb3),dis(xb2,xb3))+dis(xa3,xb3)D\max(\mathrm{dis}(x_{a1}, x_{a3}), \mathrm{dis}(x_{a2}, x_{a3})) + \max(\mathrm{dis}(x_{b1}, x_{b3}), \mathrm{dis}(x_{b2}, x_{b3})) + \mathrm{dis}(x_{a3}, x_{b3}) \geq D,当且仅当dis(xa3,xb3)=0max(dis(xb1,xb3),dis(xb2,xb3))=max(dis(xa1,xa3),dis(xa2,xa3))=D/2\mathrm{dis}(x_{a3}, x_{b3}) = 0 \land \max(\mathrm{dis}(x_{b1}, x_{b3}), \mathrm{dis}(x_{b2}, x_{b3})) = \max(\mathrm{dis}(x_{a1}, x_{a3}), \mathrm{dis}(x_{a2}, x_{a3})) = D/2时等号成立,此时xa3x_{a3}xb3x_{b3}代表的点相同(这其实是一张特殊的菊花图)。此时xa3,xb3VaVbx_{a3}, x_{b3} \in V_a \cap V_b,与假设相矛盾,故不成立。

由此,我们可以推广到多条直径的情况,结论就是:对于多条由点集V1,V2,,VnV_1, V_2, \dots, V_n构成的直径,必然有i=1nVi\bigcap\limits_{i=1}^n V_i \neq \emptyset

引理 2

设有nn条直径长度为DD,共用长度为LL的路径,各条直径上LL一侧的长度为S1,S2,,SnS_1, S_2, \dots, S_n,则有S1=S2==SnS_1 = S_2 = \dots = S_n

这也很容易证明。我们先只考虑有两条直径的情况。假设S1S2S_1 \neq S_2,且S1>S2S_1 > S_2,即第一条直径的左侧更长一些。则在两条直径共用路径的另一端,其长度为DLS2>DLS1D-L-S2 > D- L-S1,即第二条直径的右端更长一些。则我们拼接第一条直径的左端、共用路径和第二条直径的右端,得到一条长度为S1+L+DLS2=S1S2+D>DS_1 + L + D-L-S_2 = S_1-S_2 + D > D的新路径,与直径的定义矛盾,假设不成立。显然,这可以推广到有更多直径的情况。

假设我们已经通过某种方式求出了一条直径上的节点,有没有办法求出这棵树一共有多少条直径呢?

考虑节点xLx \in L,即在所有直径的共用路径上。我们考虑从已知直径的右端点、左端点作根(把直径的两端分别吊起来),分别求出根的两个位置下xx子树的最大深度以及最大深度计数,记作dep[x][0]{mx,mxcnt},dep[x][1]{mx,mxcnt}dep[x][0]\{mx, mxcnt\}, dep[x][1]\{mx, mxcnt\}。显然有dep[x][0].mx+dep[x][1].mx=Ddep[x][0].mx + dep[x][1].mx = D,否则与直径的定义相悖。此时,最大深度计数就代表着从xx向左走和向右走,分别有多少条最长路可以选择。根据乘法原理,直径总数就是两侧方案数相乘,即 D_cnt=dep[x][0].mxcnt×dep[x][1].mxcntD\_cnt = dep[x][0].mxcnt \times dep[x][1].mxcnt

所以,若是某条边处于必经边上,则其两端节点 x1,x2x_1, x_2dep[x1][0].mxcnt×dep[x1][1].mxcnt=dep[x2][0].mxcnt×dep[x2][1].mxcnt=D_cntdep[x_1][0].mxcnt \times dep[x_1][1].mxcnt = dep[x_2][0].mxcnt \times dep[x_2][1].mxcnt = D\_cnt

我们考虑一条边x1x2x_1 \leftrightarrow x_2,其中x1x_1处在非必经边上——比如共用路径LL的左侧,x2x_2处在必经边上,那么必有dep[x1][0]<dep[x2][0]dep[x_1][0] < dep[x_2][0],这是因为有部分直径已从x2x_2处分岔,不再为从x1x_1向左出发的最长链提供方案,否则这条边仍然是必经边。参见下图。

所以,现在我们只需要:

  1. 通过两遍DFS找出任意一条直径及其具体路径;
  2. 以直径两端点为根,求出所有节点的dep[x][0,1]dep[x][0,1]
  3. 依次扫描求出的这条直径上的所有边,检查其两点是否符合上述判定条件;

记变量curcur为已处理的扫描结果中合法的dep[x1][0].mxcnt×dep[x1][1].mxcntdep[x_1][0].mxcnt \times dep[x_1][1].mxcnt的最大值。显然,curcur的最大值就是D_cntD\_cnt,所以之前我认为,我们没有必要先去找出D_cntD\_cnt的具体值是多少。当某条边的dep[x1][0].mxcnt×dep[x1][1].mxcnt=dep[x2][0].mxcnt×dep[x2][1].mxcntdep[x_1][0].mxcnt \times dep[x_1][1].mxcnt = dep[x_2][0].mxcnt \times dep[x_2][1].mxcnt时,检查curcur和它的大小关系。若curcur较其小,则重置答案为ans=1ans = 1(认为只有当前这条边是必经边),并更新curcur;若curcur和它相等,则答案累加11——这是一条新的必经边。

但现在我们回过头来,看它能不能应对菊花图。假设我们有一张菊花图GG,共nn个点,11号点连接了n1n-1条边。则可以推出dep[1][0].mxcnt=dep[1][1].mxcnt=n1dep[1][0].mxcnt=dep[1][1].mxcnt=n-1。虽然会计算出错误的总直径条数,但答案仍然ansans变量的初始值00——因为没有任何一条边满足能够更新curcur的条件;但在“每个分支长度均为22的菊花图”上,这样做会出问题——直径的第一条和最后一条边将满足以上条件(参见下图)。所以,我们仍然需要先找出D_cntD\_cnt,再对每条边作判断。

每个分支长度均为2的菊花图

最终我们进行了44O(N)\mathrm{O}(N)DFS22O(N)\mathrm{O}(N)的节点扫描,最终时间复杂度为大常数的O(N)\mathrm{O}(N),非常优秀。

边权w[1,109]Nw \in [1, 10^9] \cap \mathbb{N}。记得开long long

R58557274 记录详情 (已修正)

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
#include <bits/stdc++.h> using namespace std; const int N = 204800; int n, x, y, z, e_cnt, head[N]; int p[2], from[N], path[N], p_cnt, ans; long long d[N], D, td, cur; struct data { long long mx, mxcnt; } dep[N][2]; struct edge { int v, w, nxt; } e[N<<1]; void add_e (int x, int y, int z) { e[++e_cnt].nxt = head[x], head[x] = e_cnt, e[e_cnt].v = y, e[e_cnt].w = z; } void dfs (int x, int fa, bool rec = 0) { if (td < d[x]) td = d[x], p[rec] = x; for (int i = head[x]; i; i = e[i].nxt) { if (e[i].v == fa) continue; d[e[i].v] = d[x] + e[i].w; from[e[i].v] = x; dfs (e[i].v, x, rec); } } void dp (int x, int fa, bool rec) { dep[x][rec].mxcnt = 1; for (int i = head[x]; i; i = e[i].nxt) { if (e[i].v == fa) continue; dp (e[i].v, x, rec); if (dep[e[i].v][rec].mx + e[i].w > dep[x][rec].mx) dep[x][rec].mx = dep[e[i].v][rec].mx + e[i].w, dep[x][rec].mxcnt = dep[e[i].v][rec].mxcnt; else if (dep[e[i].v][rec].mx + e[i].w == dep[x][rec].mx) dep[x][rec].mxcnt += dep[e[i].v][rec].mxcnt; } } int main () { /* 洛谷题库 P3304 [SDOI2013]直径 吴秋实 */ scanf ("%d", &n); for (int i = 1; i < n; ++i) { scanf ("%d%d%d", &x, &y, &z); add_e (x, y, z); add_e (y, x, z); } // 求出一条直径 dfs (1, 0); from[p[0]] = td = d[p[0]] = 0; dfs (p[0], 0, 1); D = td; x = p[1]; while (x) path[++p_cnt] = x, x = from[x]; // 将直径两端点分别作根,求每个点的最大深度及其计数 dp (p[0], 0, 0); dp (p[1], 0, 1); // 计算直径条数 for (int i = 1; i <= p_cnt; ++i) cur = max (cur, dep[path[i]][0].mxcnt * dep[path[i]][1].mxcnt); // 扫描并依次判定 for (int i = 2; i <= p_cnt; ++i) ans += (dep[path[i]][0].mxcnt * dep[path[i]][1].mxcnt == dep[path[i - 1]][0].mxcnt * dep[path[i - 1]][1].mxcnt) && (dep[path[i]][0].mxcnt * dep[path[i]][1].mxcnt == cur); printf ("%lld\n%d", D, ans); return 0; }

(所以换根DP到底能不能做?)

  • 2021年9月25日