題解 P3761 【[TJOI2017]城市】

update 2020/7/15 優化了一下 \(markdown\) 的用法,增長了前面的題目描述。

題目:

題目描述

從加里敦大學城市規劃專業畢業的小明來到了一個地區城市規劃局工做。這個地區一共有 \(n\) 座城市,\(n-1\) 條高速公路,保證了任意兩運城市之間均可以經過高速公路相互可達,可是經過一條高速公路須要收取必定的交通費用。小明對這個地區深刻研究後,以爲這個地區的交通費用太貴。
小明想完全改造這個地區,可是因爲上司給他的資源有限,於是小明如今只能對一條高速公路進行改造,改造的方式就是去掉一條高速公路,而且從新修建一條同樣的高速公路(即交通費用同樣),使得這個地區的兩個城市之間的最大交通費用最小(即便得交通費用最大的兩座城市之間的交通費用最小),而且保證修建完以後任意兩座城市相互可達。若是你是小明,你怎麼解決這個問題?

輸入格式

輸入數據的第一行爲一個整數 \(n\),表明城市個數。
接下來的 \(n - 1\) 行分別表明了最初的 \(n - 1\) 條公路狀況。每一行都有三個整數 \(u,v,d\)\(u,v\) 表明這條公路的兩端城市標號,\(d\) 表明這條公路的交通費用。
$ 1 \leq u,v \leq $, $ 1\leq d \leq 2000 $。

輸出格式

輸出數據僅有一行,一個整數,表示進行了最優的改造以後,該地區兩城市 之間最大交通費用。
首先由 $n \leq 5000 $ ,時間限制是 \(3s\) 咱們能夠肯定本題 \(O(n^2)\)能夠卡過去。

初步解法

咱們就能夠先有一個 \(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

\(O(n^2)\)

\(O(nL)\)

氧氣

優化二:連邊

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

原地址

座標

相關文章
相關標籤/搜索