定義:
直徑 : 在圓上兩點(不相交)之間最遠的距離就是我們通常所說的直徑。
樹的直徑 : 樹上最遠的兩個節點之間的距離就被稱為樹的直徑,連接這兩點的路徑被稱為樹的最長鏈。
求法:
1、樹形 DP
2、兩次 BFS 或者 兩次 DFS
算法 1 : 樹形 DP
優點 : 可以有效處理 負邊權
缺點 : 對於記錄路徑的信息效率較低
簡單分析 : 先通過遞歸的方式到葉子底部,然后通過自底向上的方式進行更新距離,找到最長路徑。
(看下圖,可以得到這棵樹的直徑是經過根節點 1 的 路徑最長的鏈 5 -> 2 -> 1 和 經過根節點 1 的路徑 次長鏈 3 -> 6 -> 1 兩者之和
由此可得:樹的直徑 = (經過某個節點的) 最長鏈 + 次長鏈) -- 是路徑長度哦
實現過程:
設 D[x] 表示從節點 x 出發走向以 x 為根的子樹,能夠到達的最遠距離。
設 x 的子節點為 y1,y2....yt,edge(x,y)表示邊權,顯然有: D[x] = max(D[yi] + edge(x,yi))(i 的范圍是 1 - t)
也就是說,從 根節點出發,找到自己的最小輩,然后從最小輩向根節點更新,找一個最長的路徑鏈。
只要子節點是最長的,那么我們更新到根節點時,這條鏈毫無疑問也就是最長的。
我們在找某個節點的最長鏈會發現一個問題,就是當前節點有有幾個子節點,有一個我們沒得選,當有多個時我們就需要選擇最長的。
拿上面的圖來說, 2 號節點有 兩個子節點,我們就需要進行比較一下,100 > 2 + 30 ,所以我們應該選擇 5 -> 2 這條鏈。
具體代碼:
void dp(int x) {
vis[x] = 1;
for(int i = head[x]; i ; i = Next[i]) {
int y = ver[i];
if(vis[y]) continue; // 判斷是否已經經過該節點
dp(y); // 繼續向下尋找子節點
ans = max(ans,dist[x] + dist[y] + edge[i]); // 枚舉從 x 節點出發的所有邊,找一個最遠的路徑(看上面的 2 號節點)(dist[x] 是當前目前已知的最長的,
// 但是 x 可能有多個分支,所以需要枚舉找最長的或者次長的,形成經過該節點的直徑)
dist[x] = max(dist[x],dist[y] + edge[i]); // 經過枚舉后 dist[x] 就不一定是當前最長的的,所以需要更新一下。
}
算法 2 : 兩次 DFS 或者 兩次 BFS
優點 : 可以通過一個新的數組記錄路徑信息(例如父節點與子節點之間的關系)
缺點 : 無法處理 負邊權(遇到 負邊權 涼涼)
實現過程 :
1、從任意一個節點出發,通過 BFS 或 DFS 對樹進行一次遍歷,求出與出發點距離最遠的節點,記為 p。
2、從節點 p 出發,通過 BFS 或 DFS 再進行一次遍歷,求出與 p 距離最遠的節點,記為 q。
(p 是一個節點的最遠的一個端點,那么從 p 出發的最遠的端點就是直徑的另一個端點)
為什么無法處理負邊權?
看上面這個圖: 如果按照 DFS 或者 BFS 我們第一次 找到的最遠距離的節點是 2 , 然后從 2 出發
到達的最遠距離的節點是 1 ,所以得到的樹的直徑長度是 1 ,但我們從圖中很容易看出來樹的直徑最長
應該是 2.(用樹形 DP 的話從下向上就可以得到最長的樹的直徑的長度)
具體代碼 :
BFS:
#include <queue>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 10;
int head[maxn * 2],edge[maxn * 2],Next[maxn * 2],ver[maxn * 2];
int vis[maxn],dist[maxn];
int n,p,q,d;
int tot = 0;
int maxd = 0;
int main(void) {
int BFS(int u);
void add(int u,int v,int w);
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
add(p,q,d); // 建立無向圖
add(q,p,d);
}
int u = BFS(1);
int s = BFS(u);
printf("第一次遍歷得到的節點 : %d\n",u);
printf("第二次遍歷得到的節點 : %d\n",s);
return 0;
}
void add(int u,int v,int w) {
ver[ ++ tot] = v,edge[tot] = w;
Next[tot] = head[u],head[u] = tot;
return ;
}
int BFS(int u) {
queue<int>Q;
while(!Q.empty()) Q.pop();
memset(vis,0,sizeof(vis)); // 每次遍歷的時候記得對數組進行 Clear
memset(dist,0,sizeof(dist));
Q.push(u);
int x,max_num = 0;
while(!Q.empty()) {
x = Q.front();
Q.pop();
vis[x] = 1;
for(int i = head[x]; i ; i = Next[i]) {
int y = ver[i];
if(vis[y]) continue;
vis[y] = 1;
dist[y] = dist[x] + edge[i]; // 從上向下走,所以需要進行累加(這是與 樹形 DP最大的不同)
if(dist[y] > maxd ) { // 更新 值 和 節點編號
maxd = dist[y];
max_num = y;
}
Q.push(y); // 每個新的節點都要加入到隊列中,有可能與該節點相連的路徑是比較長的
}
}
return max_num;
}
DFS :
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <string.h>
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
const int maxn = 1e5 + 10;
typedef pair<int,int> P ; // 用 pair<int,int>來保存部分信息,相對於 結構體來說更加方便一點
vector<P> G[maxn];
int dist[maxn];
int n,p,q,d;
int main(void) {
void solve();
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
G[p].push_back(make_pair(q,d));
G[q].push_back(make_pair(p,d));
}
solve();
return 0;
}
void DFS(int u,int father,int value) {
dist[u] = value; // 這種方式就不用進行對數組 Clear 了
for(int i = 0; i < G[u].size(); i ++) {
if(G[u][i].x != father) {
DFS(G[u][i].x,u,value + G[u][i].y);
}
}
return ;
}
void solve() {
DFS(1,-1,0);
int u = 1;
for(int i = 1; i <= n; i ++) { // 遍歷尋找最大值
if(dist[i] > dist[u]) {
u = i;
}
}
int x = u;
DFS(u,-1,0); // 第二次進行找另一個端點
for(int i = 1; i <= n; i ++) {
if(dist[i] > dist[u]) {
u = i;
}
}
int s = u;
printf("第一次遍歷得到的最遠的節點編號 : %d\n",x);
printf("第二次遍歷得到的最遠的節點編號 : %d\n",s);
return ;
}
參考資料:
yxc 的視頻講解:https://www.acwing.com/video/710/
秦淮岸大佬的講義:https://www.acwing.com/blog/content/319
最重要的是 AS 的細心講解。
例題:大臣的旅費
題目鏈接:https://www.acwing.com/problem/content/1209/)
題目描述:
很久以前,T王國空前繁榮。
為了更好地管理國家,王國修建了大量的快速路,用於連接首都和王國內的各大城市。
為節省經費,T國的大臣們經過思考,制定了一套優秀的修建方案,使得任何一個大城市都能從首都直接或者通過其他大城市間接到達。
同時,如果不重復經過大城市,從首都到達每個大城市的方案都是唯一的。
J是T國重要大臣,他巡查於各大城市之間,體察民情。
所以,從一個城市馬不停蹄地到另一個城市成了J最常做的事情。
他有一個錢袋,用於存放往來城市間的路費。
聰明的J發現,如果不在某個城市停下來修整,在連續行進過程中,他所花的路費與他已走過的距離有關,在走第x千米到第x+1千米這一千米中(x是整數),他花費的路費是x+10這么多。也就是說走1千米花費11,走2千米要花費23。
J大臣想知道:他從某一個城市出發,中間不休息,到達另一個城市,所有可能花費的路費中最多是多少呢?
輸入格式
輸入的第一行包含一個整數 n,表示包括首都在內的T王國的城市數。
城市從 1 開始依次編號,1 號城市為首都。
接下來 n−1 行,描述T國的高速路(T國的高速路一定是 n−1 條)。
行三個整數 Pi,Qi,Di,表示城市 Pi 和城市 Qi 之間有一條雙向高速路,長度為 Di 千米。
輸出格式
輸出一個整數,表示大臣J最多花費的路費是多少。
數據范圍
1≤n≤105,
1≤Pi,Qi≤n,
1≤Di≤1000
input:
5
1 2 2
1 3 1
2 4 5
2 5 4
output:
135
析題得說:
求路上的最大花費,最大花費由與距離有關,所以求出距離就可以解出這道題目。
實際上就是為樹的直徑是多少,只不過這道題在最后計算結果的時候還需要注意一下。
看上圖和題意,我們可以得知: 距離為 5 的花費為
s = 5;
money = s * 10 + (s * (s + 1)) / 2
具體代碼:
算法 1 : 樹形 DP
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 10;
int head[maxn],edge[maxn],Next[maxn],ver[maxn];
int dist[maxn],vis[maxn];
int tot = 0, ans = 0;
int n,p,q,d;
int main(void) {
void dp(int x);
void add(int u,int v,int w);
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
add(p,q,d);
add(q,p,d);
}
ans = 0;
dp(1);
printf("%lld\n", ans * 10 + ans * (ans + 1ll ) / 2);
return 0;
}
void add(int u,int v,int w) {
ver[++ tot] = v,edge[tot] = w;
Next[tot] = head[u],head[u] = tot;
return ;
}
void dp(int x) {
vis[x] = 1;
for(int i = head[x]; i ; i = Next[i]) {
int y = ver[i];
if(vis[y]) continue;
dp(y);
ans = max(ans,dist[x] + dist[y] + edge[i]);
dist[x] = max(dist[x],dist[y] + edge[i]);
}
return ;
}
算法 2 : 兩次 BFS
#include <queue>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1e5 + 10;
int head[maxn * 2],edge[maxn * 2],Next[maxn * 2],ver[maxn * 2];
int vis[maxn],dist[maxn];
int n,p,q,d;
int tot = 0;
int maxd = 0;
int main(void) {
int BFS(int u);
void add(int u,int v,int w);
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
add(p,q,d);
add(q,p,d);
}
int u = BFS(1);
int s = BFS(u);
printf("%lld\n", maxd * 10 + maxd * (maxd + 1ll ) / 2);
return 0;
}
void add(int u,int v,int w) {
ver[ ++ tot] = v,edge[tot] = w;
Next[tot] = head[u],head[u] = tot;
return ;
}
int BFS(int u) {
queue<int>Q;
while(!Q.empty()) Q.pop();
memset(vis,0,sizeof(vis));
memset(dist,0,sizeof(dist));
Q.push(u);
int x,max_num = 0;
while(!Q.empty()) {
x = Q.front();
Q.pop();
vis[x] = 1;
for(int i = head[x]; i ; i = Next[i]) {
int y = ver[i];
if(vis[y]) continue;
vis[y] = 1;
dist[y] = dist[x] + edge[i];
if(dist[y] > maxd ) {
maxd = dist[y];
max_num = y;
}
Q.push(y);
}
}
return max_num;
}
算法 3 : 兩次 DFS
#include <vector>
#include <cstdio>
#include <string>
#include <cstring>
#include <string.h>
#include <iostream>
#include <algorithm>
#define x first
#define y second
using namespace std;
const int maxn = 1e5 + 10;
typedef pair<int,int> P ;
struct node {
int id,w;
};
vector<P> G[maxn];
int dist[maxn];
int n,p,q,d;
int main(void) {
void solve();
scanf("%d",&n);
for(int i = 1; i < n; i ++) {
scanf("%d%d%d",&p,&q,&d);
G[p].push_back(make_pair(q,d));
G[q].push_back(make_pair(p,d));
}
solve();
return 0;
}
void DFS(int u,int father,int value) {
dist[u] = value;
for(int i = 0; i < G[u].size(); i ++) {
if(G[u][i].x != father) {
DFS(G[u][i].x,u,value + G[u][i].y);
}
}
return ;
}
void solve() {
DFS(1,-1,0);
int u = 1;
for(int i = 1; i <= n; i ++) {
if(dist[i] > dist[u]) {
u = i;
}
}
DFS(u,-1,0);
for(int i = 1; i <= n; i ++) {
if(dist[i] > dist[u]) {
u = i;
}
}
int s = dist[u];
printf("%lld\n", s * 10 + s * (s + 1ll ) / 2);
return ;
}