樹的直徑&基環樹&單調隊列
樹的直徑
定義:樹中最遠的兩個節點之間的距離被稱為樹的直徑。
怎麽求呢?有兩種官方的算法(不要問官方指誰我也不曉得):
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(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
樹的直徑&基環樹&單調隊列