1. 程式人生 > 其它 >圖論專題-學習筆記:樹的直徑

圖論專題-學習筆記:樹的直徑

目錄

1. 前言

樹的直徑是樹的一個小板塊,但是有著重要的應用。

前置知識:樹的基礎知識。

2. 詳解

例題:SP1437 PT07Z - Longest path in a tree

2.1 定義

樹的直徑:一棵樹上最長的路徑叫做樹的直徑。

比如下面這棵樹,帶有邊權 1 的路徑就是樹的直徑。

需要注意的是,這棵樹的直徑不止一條,但是一般情況下我們只取其中一條叫做這棵樹的直徑。

2.2 求法

那麼怎麼求樹的直徑呢?

這裡有兩種求法:DFS 與樹形 DP。

2.2.1 DFS 求解

該演算法的大致步驟如下:

  • 首先隨便取一個點,做一遍 DFS,求出這個點能夠到達的最遠點。
  • 然後從這個點再次 DFS,求出這個點能夠到達的最遠點。
  • 求出來的兩個點之間的路徑就是樹的直徑。

步驟簡明易懂,那麼這個演算法為什麼是正確的呢?

下面證明假設邊權大於 0。

採用反證法:

假設圖中還存在一條比我們求出來的路徑更長的路徑,那麼這條路徑就是樹的直徑。

設我們求出的路徑為 \(AB\),真正的直徑為 \(CD\)

分為兩種情況:

  1. 直徑與我們求出的路徑相交或者部分重合。

那麼根據上述演算法,我們在第一次找到點 \(A\) 的時候必有 \(AE>CE\)

同理,有 \(AE+EF+FB>AE+EF+FD\)

考慮將 \(AE>CE\) 帶入上述不等式,有 \(AE+EF+FB>CE+EF+FD\),這與 \(CD\) 是直徑不符,所以上述情況不成立。

  1. 直徑與我們求出的路徑不重合。

\(CF+FD>CF+EF+EB\),因此 \(FD>EF+EB\),則有 \(FD>EB\)

然而根據我們的演算法步驟,\(EB>EF+FD\),則有 \(EB>FD\)

出現了矛盾,因此原假設錯誤。

綜上所述,\(AB\) 必須是直徑。

那麼為什麼說這個證明必須有邊權大於 0 呢?

這是因為如果邊權小於 0,那麼上述所有證明的 \(a>b+c\rightarrow a>c\)

就都不一定成立了。

同時這也揭示了 DFS 求樹的直徑必須有所有邊權大於 0。

DFS 的優點:可以記錄樹的直徑都有哪些點,也就是可以完整記錄路徑。

DFS 的缺點:不能處理有負邊權的樹。

2.2.2 樹形 DP 求解

這個需要有一定樹形 DP 基礎,當然沒有基礎也沒有問題。

考慮設 \(f1_i,f2_i\) 分別表示第 \(i\) 個節點到葉子節點路徑的最大值與次大值。

那麼設計狀態轉移方程:

設當前的點為 \(u\),當前列舉的兒子節點為 \(v\),邊權為 \(val\),那麼有如下方程:

  • 如果 \(f1_v+val>f1_u\),那麼 \(f2_u\leftarrow f1_u,f1_u\leftarrow f1_v+val\)
  • 否則,\(f2_u=\max\{f2_u,f1_v+val\}\)

轉移方程還是簡明易懂的吧qwq

最後的答案就是 \(\max\{f1_i+f2_i|i \in [1,n]\}\)

那麼這個做法很明顯,因為是 DP 思想,可以處理具有負邊權的樹。

樹形 DP 的優點:可以處理具有負邊權的樹。

樹形 DP 的缺點:不能記錄路徑。

2.3 程式碼

兩種做法的程式碼如下(Solve_dfs 是 DFS 做法,Solve_DP 是樹形 DP 做法):

/*
========= Plozia =========
    Author:Plozia
    Problem:SP1437 PT07Z - Longest path in a tree
    Date:2021/4/27
    Another:樹的直徑模板題
========= Plozia =========
*/

#include <bits/stdc++.h>

typedef long long LL;
const int MAXN = 1e4 + 10;
int n, Head[MAXN], cnt_Edge = 1, f[MAXN], ans, root, f1[MAXN], f2[MAXN];
struct node { int Next, to, val; } Edge[MAXN << 1];

int read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
    return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
void add_Edge(int x, int y, int z) { ++cnt_Edge; Edge[cnt_Edge] = (node){ Head[x], y, z }; Head[x] = cnt_Edge; }

void dfs(int now, int father)
{
    if (f[now] > ans) { ans = f[now]; root = now; }
    for (int i = Head[now]; i; i = Edge[i].Next)
    {
        int u = Edge[i].to;
        if (u == father) continue ;
        f[u] = f[now] + Edge[i].val;
        dfs(u, now);
    }
}

void Solve_DFS()
{
    root = 1;
    f[root] = 0; dfs(root, 0); ans = 0;
    f[root] = 0; dfs(root, 0);
    printf("%d\n", ans);
}

void DP(int now, int father)
{
    for (int i = Head[now]; i; i = Edge[i].Next)
    {
        int u = Edge[i].to;
        if (u == father) continue ;
        DP(u, now);
        if (f1[u] + Edge[i].val > f1[now]) { f2[now] = f1[now]; f1[now] = f1[u] + Edge[i].val; }
        else { f2[now] = Max(f2[now], f1[u] + Edge[i].val); }
    }
}

void Solve_DP()
{
    DP(1, 0);
    for (int i = 1; i <= n; ++i) ans = Max(ans, f1[i] + f2[i]);
    printf("%d\n", ans);
}

int main()
{
    n = read();
    for (int i = 1; i < n; ++i)
    {
        int u = read(), v = read();
        add_Edge(u, v, 1); add_Edge(v, u, 1);
    }
    // Solve_DFS(); return 0;
    Solve_DP(); return 0;
}

3. 總結

樹的直徑:樹上最長路徑。

求法:

  • 兩遍 DFS 求解。
    • 優點:可以記錄路徑。
    • 缺點:不能處理負邊權。
  • 樹形 DP 求解。
    • 優點:可以處理負邊權。
    • 缺點:不能處理路徑。