1. 程式人生 > >樹的直徑&基環樹&單調隊列

樹的直徑&基環樹&單調隊列

添加 最值 技術分享 pre str bubuko text 隨著 ostream

樹的直徑

定義:樹中最遠的兩個節點之間的距離被稱為樹的直徑。

怎麽求呢?有兩種官方的算法(不要問官方指誰我也不曉得)

1.兩次搜索。首先任選一個點,從它開始搜索,找到離它最遠的節點x。然後從x開始搜索,找到離x最遠的點y,那 麽E(x, y)的長度就是樹的直徑。時間復雜度為O(n)。

2.樹形dp。這種其實更好寫。我們可以對於某個節點x,分別求出經過它的最長鏈的長度。怎麽求呢?首先,枚舉x 所連接的k個節點yi(i ∈[1,k]),都求出以yi為根的子樹最大深度d[yi]。那麽,經過x的最長鏈長度f[x] = max{d[yi] + E(x, yi)}(i ∈[1, k])。~好理解吧~

那麽這個算法的復雜度就是O(n)。

實際上,搜索求直徑的復雜度為O(2n),是樹形dp的兩倍。不過影響不大,並且搜索可以節省內存。搜索美滋滋。

//搜索求解
int mmax, pos;
void dfs(int u, int pre, int w){
    if(w >= mmax){
        mmax = w;
        pos = u;
    }
    int v;
    for(int i = head[u]; ~i; i = e[i].next){
        v = e[i].to;
        if(v != pre) dfs(v, u, w + e[i].dis);
    }
}
//main函數中 dfs(1, 0, 0); dfs(pos, 0, 0);
//dp求解
int d[maxn], f[maxn], ans;
bool vis[maxn];
void dp(int u){
    vis[u] = true;
    int v;
    for(int i = head[u]; ~i; i = e[i].next){
        v = e[i].to;
        if(!vis[v]){
            dp(v);
            ans = max(ans, d[u] + d[y] + e[i].dis);
            d[u] 
= max(d[u], d[y] + e[i].dis); } } } //main函數中 dp(1);

技術分享圖片

技術分享圖片

我們首先證明一個定理:

給定一棵樹,對於它的任一直徑,若取其幾何意義上的中點,叫做這條直徑的中點。那麽, 一棵樹的所有直徑的中點必定是同一點。

證明:

顯然,當這棵樹只有一條直徑時,定理成立。但很多情況下一棵樹不止一條直徑。但所有直徑必定都經過同一點。為 什麽呢?(下面證明的過程中有關長度與距離之類的量都為幾何意義上的量)

假設有兩條直徑不經過同一點。設兩條直徑分別有一點A,B,並且A能夠在不經過這兩條直徑上的邊的情況下到達B。 那麽這兩個節點就將所在的直徑分成了兩段。我們取每條直徑上較長的那段,加上A和B之間的長度,顯然大於原來 的直徑長度。

那麽任意兩條直徑必定會相交於一點。現在再假設有兩條直徑滿足它們的中點A,B不是同一點。顯然對於中點來說, 它將其所在直徑分成了相同長度的兩段。不妨設有一條不經過直徑的路徑連接A和另一條直徑上的C點,那麽:A所在 直徑長度的1/2 + E(A,C) + E(B,C) + B所在直徑長度的1/2 ≥ 任意一條直徑的長度 = A所在直徑長度的1/2 + B所在直徑 長度的1/2,這樣就存在一條新的路徑,其長度大於原來的直徑,這有悖直徑定義。

證畢。

(Φ皿Φ)證出來了~

回到直徑這道題,我們可以先隨便求一條直徑出來,然後設法搞出它的中點(直徑長度/2的位置)。現在有兩種情況:

1.(軟柿子)中點就是樹的某個節點。我們可以直接以這個點為根進行計算。

2.中點在樹的某條邊上。醬紫的話就把這條邊扔了,在剩下的兩棵子樹上計算。

怎麽計算呢?

第一種情況,由於從根節點可以連接出許多棵子樹。我們取出深度最大的三棵子樹a,b,c,並用d[x]表示x子樹的最大 深度。

1.d[a] > d[b] > d[c]。顯然直徑會經過a子樹和b子樹。然後我們遞歸,在a子樹和b子樹的根上進行相同計算,算出直 徑必然經過的邊。

2.d[a] > d[b] = d[c]。那麽只需在a子樹的根上進行相同計算即可。

3.d[a] = d[b] = d[c]。那麽沒有被所有直徑經過的邊。

如法炮制,對於第二種情況,我們只需在去掉邊之後的兩顆子樹上分別進行上述操作即可。

總結與拓展:

對於樹的直徑問題,我們會有兩種做法:搜索與dp,兩者復雜度相似。在看過例題“直徑”之後我們可以證明出了一條 定理:一棵樹的所有直徑必定以同一點為中點。實際上,在做樹上問題,特別是樹的直徑時,證明是少不了的。當然看上去正確的定理水一水也就過了。然而,樹的直徑通常不會單獨在一道題裏出現,它經常伴隨著其它的算法, 如“直徑”中最後計算時整的什麽鬼算法,還有經常會和樹的直徑一起用的二分答案。多練吧~

基環樹

顧名思義,基環樹就是基於環的樹。

對於一棵樹,若它有n個點,則一定有n-1條邊。而如果在其之上添加一條邊,那麽就會形成一個環(好理解吧~溜 ~)。加了環之後,這個什麽鬼就叫基環樹。

基環樹的題目並不多,但是對於大部分的題,我們可以很容易判斷出它的圖是否為基環樹。通常會有兩種特征:

1.點數為n,邊數也為n

2.且每條邊都至少連出去一條邊(出度≥0)

技術分享圖片

舉個例子:

eg.1

技術分享圖片

技術分享圖片

技術分享圖片

很明顯的基環樹

eg.2

技術分享圖片

技術分享圖片

簡直在告訴我們它是基環樹

通過這兩道考試的原題你可以看出,基環樹是多麽重要的一個亂七八糟的結構

那麽,對於基環樹的問題,我們怎麽解決呢?(這裏直接講剛才的例題因為沒有官方算法

第一題就先不看了

我們看島嶼這題:

技術分享圖片

這題顯然是有不只一棵基環樹,因此我們可以對每棵樹都求一下可以走過的最大長度。

那麽,這就有點類似於求樹的直徑了。那麽我們引入一個新的概念(不是官方的):基環樹的直徑——基環樹上最長 的簡單路徑(不自交的路徑)。其實也是最遠兩點之間的距離(這樣會比較高端)

如何求呢?對於一棵基環樹,它的直徑會有兩種情況:

1.去掉環上所有邊之後的某棵子樹的直徑。

2.環上分別以兩點為根的兩棵子樹的直徑加上它們環上的距離,這個環上距離可以是逆時針的,也可以是順時針的。

所以,想要AC島嶼這道題,我們只需要求出每棵基環樹的直徑,再累加起來就是答案了。

怎麽求呢?——為了逃避第二種情況我們先求解第一種情況。

顯然,我們可以首先dfs一次圖,找出圖中的環。然後枚舉環上每個點,分別求出以這個點為根的子樹的直徑d[i]。那 麽答案就累加上直徑。

第二種情況。我們枚舉環上每個點,分別求出以之為根的子樹的最大深度d[i]。那麽答案就累加上max{d[i] + d[j] + dist(i, j)},其中i,j∈環,dist(i, j)表示i和j的環上距離(順時針和逆時針的較大者)。怎麽算dist呢?可以用一個熟悉的 技巧——拆環(合並石子裏面的),即把環從一個點斷開並拉成一條鏈,再把這條鏈復制一份塞到後面去。然後枚舉這 條鏈上的點,算出其前綴和sum[x],那麽dist(i, j) = max(sum[i] - sum[j], sum[j + lenth] - sum[i])。其中lenth是 環的長度。但不用這麽算,因為枚舉得到點j+lenth。所以,ans += {d[i] + d[j] + sum[i] - sum[j]}。那麽這個算法 就是O(n²)。然而數據範圍是10^6,這種算法顯然過不了。那就不做了。不過優化是有的。怎麽優化呢?單調隊列牛 批!

所以我們先學一下單調隊列

單調隊列

何為單調隊列?答:單調的隊列。(啪!)

單調隊列是用來找區間最值用的又一個亂七八糟的數據結構。可以STL裏面的雙端隊列deque來實現,但那種方式的 開銷不如手寫的少。怎麽找最值呢?我們來看看單調隊列通常用來解決的問題:

給定一個數列a[n],求出區間[i - k, i]中的最大值,其中i∈[1, n], k為常數。

我們首先開一個數組,並用兩個int變量作為它的首尾l和r。我們枚舉數列的每個下標。當枚舉到i下標時,我們將a[i] 入隊,並判斷一下a[i]是否大於之前隊列的隊尾值。若是大於,那就有悖單調隊列的定義(單調的隊列),那就不停將 隊尾出隊,直到q[r] >= a[i]為止。這樣就維護了隊列的單調性。再看,根據題意,我們只需要計算長度為k + 1的區 間。所以,我們再判斷一下:若l < r - k,那麽將l加到r - k為止。那麽,一個單調隊列就維護好了。

給出代碼:

#include <iostream>
#define maxn 1000000 + 5
using namespace std;
 
int a[maxn], n, k;
int list[maxn], l, r;
 
int main(){
    scanf("%d%d", &n, &k);
    for(int i = 1; i <= n; i++){
        scanf("%d", &a[i]);
    }
    l = r = 0;
    for(int i = 1; i <= n; i++){
        while(l <= r && list[r] < a[i]) r--;
        list[++r] = a[i];
        while(l <= r && l < r - k) l++;
        printf("%d\n", list[l]);
    }
    return 0;
}
Sample in:
7 4
7 6 8 12 9 10 3

最小值的求法也是類似的~

封墳————————————

挖~

然後我們回到島嶼這題。看到之前得出的表達式:ans += {d[i] + d[j] + sum[i] - sum[j]}。其中,我們只需枚舉i∈[1, n],然後對於已經枚舉過的j∈[1, i - 1],我們可以用單調隊列來維護這段區間的max(d[j] - sum[j])。然後,因為我 們復制了環,所以當i > lenth時,我們只需在[i - lenth + 1, i - 1]裏面找最大值即可。這些就巧妙地對應了剛才單調 隊列裏面的所有操作,因此整個算法的復雜度為O(n)。快不快?

總結:

對於基環樹的問題,我們一般的做法是先處理環上部分,再處理以環上節點為根的每棵子樹。但這也不是普遍適用 的,比如:

技術分享圖片

就是這貨,它的解法是從一點開始貪心,找到環時再特判一下去最優值,根本不是上面所的一般情況,所以這裏不做 過多討論。其實是沒AC

End

樹的直徑&基環樹&單調隊列