咱们就能够先有一个 \(O(n^2)\) 的暴力解法。
(这一版基本是照着某一楼的题解打出来的)
咱们枚举每一条边断开,而后求连个联通块各自的直径,以及两个联通块的最短半径,基本能够说是半个纯暴力。数组
void Diameter(const int u)//找直径的函数 { book[u] = 1;//用来标记是否遍历过。 for(reg int i = head[u]; i ; i = e[i].next) if(!book[e[i].to]) { Diameter(e[i].to); int v = f[e[i].to][0] + e[i].wi; if(v > f[u][0]){f[u][1] = f[u][0];f[u][0] = v;mv[u] = e[i].to;} else if(v > f[u][1]){f[u][1] = v;} } diameter = Max(diameter,f[u][1] + f[u][0]);//很标准的一个求树直径的 DP。 } void Radius(const int u,const int front)//找半径的函数 { // front 用来记录自身子树内的最短半径。 book[u] = 0;radius = Min(radius,Max(front,f[u][0])); for(reg int i = head[u]; i ; i = e[i].next) if(book[e[i].to]) Radius(e[i].to,Max(front,mv[u] == e[i].to ? f[u][1] : f[u][0]) + e[i].wi); } int main() { n = Read(); for(reg int i = 1; i < n ; ++i) add_edge(Read(),Read(),Read()); for(reg int i = 2; i <= tot_edge; i += 2) { int d1,d2,r1,r2; diameter = 0; book[e[i].to]=1; Diameter(e[i^1].to); d1 = diameter; diameter = 0; Diameter(e[i].to); d2 = diameter; book[e[i^1].to]=0; radius = INF; Radius(e[i].to,0); r1 = radius; radius = INF; Radius(e[i^1].to,0); r2 = radius; Ans = Min(Ans,Max(Max(d1,d2),r1+r2+e[i].wi)); for(reg int i = 1 ; i <= n; ++i) {f[i][0] = mv[i] = f[i][1] = book[i] = 0;} } printf("%d",Ans); return 0; }
断的边必定在原来树的直径上,且是树全部直径的公共边。markdown
对于非直径上的边,就算断掉,剩下的两个联通块的直径有一个仍是原来的直径,因此对其咱们要求的答案无影响。函数
而后直径的非公共边。
如图树的直径有两条, $ 1->8 $ 和 $ 1->9 $ ,断掉 $ 5->6,5->7,6->9,7->8$ 中的任意一条,都不会让剩下的两个联通块的直径减少,因此其对答案也无影响。
(这里的性质使选原树任意一条直径进行删边均可以找到正确答案所删的那一条边)优化
由此咱们能够获得一个优化, 时间复杂度是 $ O(nL)$ , \(L\) 是原树直径的边数。spa
void dfs(const int u,const int fa) { for(reg int i = head[u]; i ; i = e[i].next) if(e[i].to != fa) { dis[e[i].to] = dis[u] + e[i].wi; mv[e[i].to] = i; dfs(e[i].to,u); } } void Diameter(const int u) { book[u] = 1; for(reg int i = head[u]; i ; i = e[i].next) if(!book[e[i].to]) { Diameter(e[i].to); int v = f[e[i].to][0] + e[i].wi; if(v > f[u][0]){f[u][1] = f[u][0];f[u][0] = v;mv[u] = e[i].to;} else if(v > f[u][1]){f[u][1] = v;} } diameter = Max(diameter,f[u][1] + f[u][0]); } void Radius(const int u,const int front) { book[u] = 0;radius = Min(radius,Max(front,f[u][0])); for(reg int i = head[u]; i ; i = e[i].next) if(book[e[i].to]) Radius(e[i].to,Max(front,mv[u] == e[i].to ? f[u][1] : f[u][0]) + e[i].wi); } int main() { n = Read(); for(reg int i = 1; i < n ; ++i) add_edge(Read(),Read(),Read()); dfs(1,1); for(reg int i = 1; i <= n ; ++i) if(dis[S] < dis[i]) S = i; dis[S] = 0; for(reg int i = 1; i <= n ; ++i) mv[i] = 0; dfs(S,S); for(reg int i = 1; i <= n ; ++i) if(dis[T] < dis[i]) T = i; for(reg int i = mv[T]; i ; i = mv[e[i^1].to]) ded[++tde] = i; for(reg int i = 1; i <= n ; ++i) mv[i] = 0; for(reg int i = 1; i <= tde; i++)//可优化,只删直径 { int d1,d2,r1,r2; diameter = 0; book[e[ded[i]].to]=1; Diameter(e[ded[i]^1].to); d1 = diameter; diameter = 0; Diameter(e[ded[i]].to); d2 = diameter; book[e[ded[i]^1].to]=0; radius = INF; Radius(e[ded[i]].to,0); r1 = radius; radius = INF; Radius(e[ded[i]^1].to,0); r2 = radius; Ans = Min(Ans,Max(Max(d1,d2),r1+r2+e[ded[i]].wi)); for(reg int i = 1 ; i <= n; ++i) {f[i][0] = mv[i] = f[i][1] = book[i] = 0;} } printf("%d",Ans); return 0; }
从 $ 17.55s -> 1.61s $,挂了氧气能达到 \(871ms\) 。code
\(1.\) 2若是连的是直径上的点,那么能够肯定新树的直径是两个联通块直径上的较长链相加,为了使其尽量短,因此咱们要连两个联通块直径的中点来使较长链更短。blog
\(2.\) 若是连的不是直径上的点,那么能够肯定新树的直径是两个联通块直径上的较长链相加在加上链接点到各自直径的距离,是必定长于 方案 \(1\) 的。资源
因此能够写一个找直径中点的函数代替上文中找半径的函数。get
这个函数时间复杂度很难算,姑且可当作 \(\Omega(1)\) ,卡一卡就变 \(O(L)\) 了。io
能够证实的是联通块上的直径必定有一半以上的长度是与原树直径重合的(只须要理解一下上文用 \(DP\) 求直径的作法),能够用这个性质来找中点。
这个优化代码我没单独写
int rt=0,lt=0,Half = ans>>1,cur; cur = i; while(dp[cur][0] - WW[cur] > Half && cur) cur = mvv[cur]; rt = dp[cur][0]; cur = mv[i]; Half = (f[mv[i]][0] + f[mv[i]][1])>>1; while(f[cur][0] - W[cur]> Half && cur) cur = mv[cur]; lt = f[cur][0]; ans = Max(ans,W[i] + lt + rt);
调了好久也没调出来。
咱们在直径上遍历删边的时候,不难发现作了不少的重复的遍历。
在找右边直径的过程都是能够经过 \(O(n)\) 预处理变成 \(O(1)\) 的。
在找左边直径的过程能够用 \(book\) 数组标记,不重复遍历,也能够实现总体 \(O(n)\)的。
最终加上连边的优化是能够达到 \(\Omega(n)\)?
须要特别注意的是,会有特殊的数据如图:
就是如图所示,删去 \(6 -> 1\) 的边后最长链不通过 \(1\) 点,这须要特殊处理。
即断的边的端点不必定在断边后联通块的直径上。
个人想法就是先找到最长链的两个端点,再分别从两个端点跑一次 \(dfs\) 。
要记录两个东西。
当前子树直径。
据当前子树根节点最近的直径上的节点。
void dfs1(const int u,const int fa) { for(reg int i = head[u]; i ; i = e[i].next) if(e[i].to != fa) { dfs1(e[i].to,u); int v = f[e[i].to][0] + e[i].wi; if(v > f[u][0]){f[u][1] = f[u][0];f[u][0] = v;mv[u] = e[i].to;W[u] = e[i].wi;} else if(v > f[u][1]){f[u][1] = v;} A[u] = Max(A[u],A[e[i].to]); } A[u] = Max(A[u],f[u][1] + f[u][0]); } void dfs(const int u,const int fa) { for(reg int i = head[u]; i ; i = e[i].next) if(e[i].to != fa) { dfs(e[i].to,u); int v = dp[e[i].to][0] + e[i].wi; if(v > dp[u][0]){dp[u][1] = dp[u][0];dp[u][0] = v;mvv[u] = e[i].to;WW[u] = e[i].wi;} else if(v > dp[u][1]){dp[u][1] = v;} B[u] = Max(B[u],B[e[i].to]); } B[u] = Max(B[u],dp[u][1] + dp[u][0]); }
记录每个子树的最长链,次长链,而后断的边移动,可是不用 \(DP\) 了,能够直接从数组中找到当前状况下各联通块的直径,最后找一下对应直径中点就能够找到答案了。