洛谷题库 P3304 [SDOI2013]直径
想看正解请直接跳过文章前半段。
不知道到底错没错的错解
这是一道树的直径的必经边问题。
单单求树的直径是非常简单的。可以通过两次DFS/BFS或一次树形DP完成O(N)的求解。显然,树的直径长度唯一,但构成该长度的链的方案可能不唯一。
一开始,我想通过两次树形DP完成求解:假设我们已求出直径长为D。设f[x]{mx,mxcnt}记录在以1号节点作根时节点x的子树上的最大深度及其计数(深度最大的链可能不止一条),显然有f[x]={f[soni]{f[x].mx,f[x].mxcnt+f[soni].mxcnt}f[soni].mx>f[x].mxf[soni].mx=f[x].mx进而通过换根DP求出F[x]{mx,mxcnt},即在以x为整棵树的根时的最大深度和计数(求解方法类似于f[x])。求解F[x]时,x的父亲节点fa的F[fa]应当已经求出,这时如果边x↔fa在直径上,则有f[x].mx+F[fa].mx=D,且通过该边的直径条数应该是cnt=f[x].mxcnt×F[x].mxcnt。如果它是一条必经边,则有cnt≥maxcnt,其中maxcnt是全局中cnt的最大值。若有cnt≥maxcnt,则说明“cnt才是直径条数”,ans重置为1;若有cnt=maxcnt,则ans自增1。
乍看,这个方法好像很有道理。正准备实现的时候,再一细想却傻眼了:
- 如果F[fa]在转移时采取的是包含x的方案(即以fa作根时,最大深度的那条链恰好包含现在扫描到的x),怎么办?
- 在菊花图上使用该方法,您将顺利得到所有的边都是直径必经边的错误结果。
对于第一个问题,我曾考虑过记录次大的F[x]和f[x],转移时保证采取的F[x]不包含当前所在链的方案。但是实现起来太太太复杂了……还非常容易写错。至于为什么会出现第二个问题……请看下面的菊花图。
一个根节点度为3的菊花图
显然,该图共有3条直径2↔1↔4、2↔1↔3和3↔1↔4,且必经边条数为0。采取如上方法计算,会得到cnt2=cnt3=cnt4=maxcnt=3,导致三条边全部计入答案。
所以这个花了我两个晚自习加一个小时的方法报废了。
正解
在说明正解之前,需要捣鼓几条推论。
引理 1
若树的直径方案不唯一,则设一条直径的点集为Va,另一条直径的点集为Vb,有Va∩Vb=∅。
采用反证法证明。假设有Va∩Vb=∅。设两条直径两端点分别为xa1,xa2和xb1,xb2,则由树上两点之间有且仅有一条简单路径可知,必然存在路径xa3⟷xb3 (xa3∈Va,xb3∈Vb),使得两直径上任意两点相互可达。在两条直径上,xa1,xa2和xa3的距离、xb1,xb2和xb3的距离分别记作dis(xa1,xa3),dis(xa2,xa3),dis(xb1,xb3)和dis(xb2,xb3)。容易知道,有max(dis(xa1,xa3),dis(xa2,xa3))≥D/2,max(dis(xb1,xb3),dis(xb2,xb3))≥D/2。则取max(dis(xa1,xa3),dis(xa2,xa3))+max(dis(xb1,xb3),dis(xb2,xb3))+dis(xa3,xb3)≥D,当且仅当dis(xa3,xb3)=0∧max(dis(xb1,xb3),dis(xb2,xb3))=max(dis(xa1,xa3),dis(xa2,xa3))=D/2时等号成立,此时xa3与xb3代表的点相同(这其实是一张特殊的菊花图)。此时xa3,xb3∈Va∩Vb,与假设相矛盾,故不成立。
由此,我们可以推广到多条直径的情况,结论就是:对于多条由点集V1,V2,…,Vn构成的直径,必然有i=1⋂nVi=∅。
引理 2
设有n条直径长度为D,共用长度为L的路径,各条直径上L一侧的长度为S1,S2,…,Sn,则有S1=S2=⋯=Sn。
这也很容易证明。我们先只考虑有两条直径的情况。假设S1=S2,且S1>S2,即第一条直径的左侧更长一些。则在两条直径共用路径的另一端,其长度为D−L−S2>D−L−S1,即第二条直径的右端更长一些。则我们拼接第一条直径的左端、共用路径和第二条直径的右端,得到一条长度为S1+L+D−L−S2=S1−S2+D>D的新路径,与直径的定义矛盾,假设不成立。显然,这可以推广到有更多直径的情况。
假设我们已经通过某种方式求出了一条直径上的节点,有没有办法求出这棵树一共有多少条直径呢?
考虑节点x∈L,即在所有直径的共用路径上。我们考虑从已知直径的右端点、左端点作根(把直径的两端分别吊起来),分别求出根的两个位置下x的子树的最大深度以及最大深度计数,记作dep[x][0]{mx,mxcnt},dep[x][1]{mx,mxcnt}。显然有dep[x][0].mx+dep[x][1].mx=D,否则与直径的定义相悖。此时,最大深度计数就代表着从x向左走和向右走,分别有多少条最长路可以选择。根据乘法原理,直径总数就是两侧方案数相乘,即 D_cnt=dep[x][0].mxcnt×dep[x][1].mxcnt。
所以,若是某条边处于必经边上,则其两端节点 x1,x2 之 dep[x1][0].mxcnt×dep[x1][1].mxcnt=dep[x2][0].mxcnt×dep[x2][1].mxcnt=D_cnt
我们考虑一条边x1↔x2,其中x1处在非必经边上——比如共用路径L的左侧,x2处在必经边上,那么必有dep[x1][0]<dep[x2][0],这是因为有部分直径已从x2处分岔,不再为从x1向左出发的最长链提供方案,否则这条边仍然是必经边。参见下图。
所以,现在我们只需要:
- 通过两遍DFS找出任意一条直径及其具体路径;
- 以直径两端点为根,求出所有节点的dep[x][0,1];
- 依次扫描求出的这条直径上的所有边,检查其两点是否符合上述判定条件;
记变量cur为已处理的扫描结果中合法的dep[x1][0].mxcnt×dep[x1][1].mxcnt的最大值。显然,cur的最大值就是D_cnt,所以之前我认为,我们没有必要先去找出D_cnt的具体值是多少。当某条边的dep[x1][0].mxcnt×dep[x1][1].mxcnt=dep[x2][0].mxcnt×dep[x2][1].mxcnt时,检查cur和它的大小关系。若cur较其小,则重置答案为ans=1(认为只有当前这条边是必经边),并更新cur;若cur和它相等,则答案累加1——这是一条新的必经边。
但现在我们回过头来,看它能不能应对菊花图。假设我们有一张菊花图G,共n个点,1号点连接了n−1条边。则可以推出dep[1][0].mxcnt=dep[1][1].mxcnt=n−1。虽然会计算出错误的总直径条数,但答案仍然ans变量的初始值0——因为没有任何一条边满足能够更新cur的条件;但在“每个分支长度均为2的菊花图”上,这样做会出问题——直径的第一条和最后一条边将满足以上条件(参见下图)。所以,我们仍然需要先找出D_cnt,再对每条边作判断。
每个分支长度均为2的菊花图
最终我们进行了4次O(N)的DFS和2次O(N)的节点扫描,最终时间复杂度为大常数的O(N),非常优秀。
边权w∈[1,109]∩N。记得开long long
。
R58557274 记录详情 (已修正)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 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 () {
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到底能不能做?)