注:文章爲博主原創,從git上的博客搬運而來, 原地址
樹的重心也叫樹的質心。找到一個點,其全部的子樹中最大的子樹節點數最少,那麼這個點就是這棵樹的重心,刪去重心後,生成的多棵樹儘量平衡。git
給樹作分治的時候,爲了獲得最佳的劃分,讓這棵樹更加平衡。算法
樹的重心也叫樹的質心。對於一棵樹n個節點的無根樹,找到一個點,使得把樹變成以該點爲根的有根樹時,最大子樹的結點樹最小。換句話說,刪除這個點後最大連通塊(必定是樹)的結點數最小。
以這個點爲根,那麼全部的子樹(不算整個樹自身)的大小都不超過整個樹大小的一半
不少博客使用了上述兩種定義,但只是說了定義,沒有講來歷,今天這裏就來分析一番。
上面給出了平衡
這個詞語,什麼叫作平衡呢?
什麼樣的樹就看起來更加平衡呢?
定義
$$k_i = n - 2P_i $$
$P$ 表示把這棵樹(無根樹)以 某節點(假設編號爲i)轉化爲有根樹的後,節點數最大的子樹的節點數(下同)
$n$ 表示總節點數
$k$ 表示平衡度
變形爲
$$ k_i = n - 2P_i $$
$$ k _ i = n - P_i - (n - O_i - 1) $$
$$ k_i = O_i - P_i + 1$$
$O$表示除了節點最多子樹和 i 自己之外其餘節點數的總和
$k$ 越大,這棵樹能夠被認爲是更加平衡
那麼給出定義:spa
$k$ 不小於0的時候,該點就是樹的重心
對於定義二
,這是顯然的,由於$2P \leq n$,因此$ k = n - 2P \geq 0 $code
對於定義一
,它就顯得不那麼好證實了,
因此這裏也給出證實:
充分性:
假設以這個點 $i$ 爲根,轉化爲有根樹,則會獲得:
$P$,$O$的含義同上。
由定義一
這裏的$P$必定是對於全部其餘節點而言的$P$最小的。
考慮最大子樹的根 maxv ,假設去掉maxv
則會獲得:rem
考慮這個此時對於$maxv$來講的$P$
有:
狀況1:新的 $P'$是原子樹$maxv$中的子樹。
狀況2:新的 $P'$是 $O+1$get
對於狀況1:若是新的$P'$是原子樹中的子樹,設這個子樹的節點數爲$P_m$,則有$$P_m = P-1$$,而定義一
知足的條件是$P$小於其餘任意一個的$P'$,這明顯不知足條件。博客
對於狀況2:新的$P'$是$O+1$,
$ P' \geq P $
$ O + 1 \geq P$
$ O - P + 1 \geq 0$
$ k \geq 0$
得證。string
必要性:
$k \geq 0$
$O + 1 \geq P$
考慮當前節點$i$ 的 最大子樹節點樹$P$
考慮最大節點子樹$P$的節點,從該節點往下,每個節點的子孫節點都有大於等於以 $i$爲子樹的$O+1$個節點,這個節點的$P'$大於等於$P$。
考慮節點$i$非最大子樹的節點,這些節點都至少有以$i$爲根的$P+1$個節點,這些節點的$P'$值必定大於$P$
綜上所述,對於$i$,它的最大子樹是全部節點中最小的
得證。
因此說,其實本質上而言,兩種定義都是對平衡度的限制,it
樹形DP,dfs一次便可,保存每一個節點的子樹節點數,計算出後計算最大子樹$P$和$O$,取全部點中最小的便可.
代碼(POJ 1655):io
#include<cstdio> #include<cstring> #define emax(a,b) ((a) < (b) ? (b) : (a)) using namespace std; const int MAXN = 20005; const int MAXE = 40005; const int INF = 0x3f3f3f3f; struct edge{ int v,next; }; edge e[MAXE]; int head[MAXN],subTree[MAXN],dp[MAXN]; int T,k,n,maxw,maxu; char inc; inline void adde(int u,int v){ e[k].v = v; e[k].next = head[u]; head[u] = k++; } inline void read(int & x){ x = 0; inc = getchar(); while(inc < '0' || inc > '9'){ inc = getchar(); } while(inc >= '0' && inc <= '9'){ x = (x << 3) + (x << 1) + (inc ^ 48); inc = getchar(); } } void dfs(int u,int fa){ subTree[u] = 1; for(int i = head[u];~i;i = e[i].next){ int v = e[i].v; if(v == fa) continue; dfs(v,u); subTree[u] += subTree[v]; dp[u] = emax(dp[u],subTree[v]); } dp[u] = emax(dp[u],n - subTree[u]); } int main(){ read(T); while(T--){ read(n); memset(head,-1,sizeof(head)); k = 1; memset(dp,0,sizeof(dp)); for(int i = 2;i <= n;i++){ int u,v; read(u);read(v); adde(u,v); adde(v,u); } dfs(1,0); maxw = INF; maxu = -1; for(int i = 1;i <= n;i++){ if(dp[i] < maxw){ maxu = i; maxw = dp[i]; } } printf("%d %d\n",maxu,maxw); } return 0; }
1.定義距離和值爲
$$S_j = \sum D_{i,j}$$
其中$D_{i,j}$表示$i$到$j$的距離
那麼對於重心,它的 $S_j$ 值最小。若是有兩個重心,他們的 $S_j$ 值相同
2.把兩個樹經過一條邊相連獲得一個新的樹,那麼新的樹的重心在鏈接原來兩個樹的重心的路徑上。
3.把一個樹添加或刪除一個葉子,那麼它的重心最多隻移動一條邊的距離。
4.以u爲根的一顆樹,若是有某兒子:該兒子的子樹節點個數和的二倍>以u爲根的樹的節點的個數,那麼重心在以v爲根的子樹之中。
四個性質都很好證實(畫個圖就能看出),這裏不加贅述
注:對於性質1,參考算法導論中對中位數的絕對值和最小的證實
參見Codeforeces 686D
題目大意是讓你求出這棵樹全部子樹的重心。
根據性質2,只須要從$maxv$爲根的重心不斷向上,直到找到重心爲止:
#include<cstdio> #include<cstring> using namespace std; const int MAXN = 300005; const int MAXE = 600005; struct edge{ int v,next; }; edge e[MAXE]; char inc; int subTree[MAXN],ans[MAXN],fa[MAXN],head[MAXN]; int k,n,q; inline void adde(int u,int v){ e[k].v = v; e[k].next = head[u]; head[u] = k++; } inline void read(int & x){ x = 0; inc = getchar(); while(inc < '0' || inc > '9'){ inc = getchar(); } while(inc >= '0' && inc <= '9'){ x = (x << 3) + (x << 1) + (inc ^ 48); inc = getchar(); } } void dfs(int u){ subTree[u] = 1; ans[u] = u; int maxSubTree = -1; int maxSubTreev = -1; for(int i = head[u];~i;i = e[i].next){ int v = e[i].v; dfs(v); if(maxSubTree < subTree[v] ){ maxSubTree = subTree[v]; maxSubTreev = v; } subTree[u] += subTree[v]; } if(maxSubTree * 2 <= subTree[u]) return; int c = ans[maxSubTreev]; while(subTree[u] > subTree[c] * 2){ c = fa[c]; } ans[u] = c; } int main(){ memset(head,-1,sizeof(head)); read(n);read(q); for(int i = 2;i <= n;i++){ int v; read(v); //adde(i,v); adde(v,i); fa[i] = v; } dfs(1); for(int i = 1;i <= q;i++){ int qu; read(qu); printf("%d\n",ans[qu]); } return 0; };