咱們就能夠先有一個 \(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\) 了,能夠直接從數組中找到當前狀況下各聯通塊的直徑,最後找一下對應直徑中點就能夠找到答案了。