POJ 1741 Tree, 樹的重心, 樹分治, 點分治
最近在學習樹的分治,算是比較難,而且程式碼量比較大的一塊。隨便拿一道題來就有上百行,故寫一篇文章來總結一下這方面的框架。
POJ這一題應該算是樹分治的入門題,順便用這一題來詳細說明樹分治的一些具體內容。
Tree Time Limit: 1000MS Memory Limit: 30000KDescription
Give a tree with n vertices,each edge has a length(positive integer less than 1001).Define dist(u,v)=The min distance between node u and v.
Give an integer k,for every pair (u,v) of vertices is called valid if and only if dist(u,v) not exceed k.
Write a program that will count how many pairs which are valid for a given tree.
Input
The last test case is followed by two zeros.
Output
For each test case output the answer on a single line.Sample Input
5 4 1 2 3 1 3 1 1 4 2 3 5 1 0 0
Sample Output
8
題目大意:有一個有n個節點、帶邊權的樹(n<=10000);只有一次詢問,求樹上路徑長度<=k的所有路徑條數。每個測試點有多組資料。
首先此題有顯然的N^2做法:用rmq或者lca預處理,然後列舉所有點對,但這樣顯然超時。
因此,在這裡我們考慮樹上分治的做法:
1.轉化問題:我們將這棵樹轉化為一棵有根樹,這樣就可以統計 經過一個根節點 且只經過其子樹中的節點的 合法路徑 的條數,將經過各個點的這樣的路徑數加起來既是答案;如圖,綠色的路徑就是一條我們要求的路徑;
2.如何得出經過每一個根結點的合法路徑數呢?對於每一棵子樹(如圖中方框中 以紅色結點為根的子樹),我們dfs求出這棵子樹中所有節點到紅色根節點的距離,然後將這些距離放在dep陣列中sort一下,這樣就可以顯然地求出這棵子樹中
3.但這樣求出的不是經過一個節點的合法路徑數,而是整個子樹中的合法路徑數;這些路徑中,有些路徑並不經過根節點;顯然,如果這樣計算求和,會有很多路徑被重複計算。為了去除重複,我們對紅色根節點的所有兒子節點做和第2步相同的操作,並且將每個兒子的結果都剪掉;這樣就剪掉了所有隻經過子樹中的結點、而不經過根節點的路徑數。減去之後,剩下的自然就是我們要求的經過根節點的路徑數。
以上就是我們計算路徑條數時的主要思想。為了降低複雜度,就要降低每次dfs時子樹中結點的個數。這時我們就用到分治的思想:遞迴處理,每次找到重心,進行2、3步的操作;再將這個點挖掉,對剩下的子樹再找重心,進行2、3步的操作,以此遞迴。利用重心的性質,每個子樹都至少減小到上一級子樹的一半,於是複雜度就降到了log級別。
值得一提的是,第3步的去重思想,在樹上分治的題中有廣泛且靈活的應用;本題較為基礎,初學者應該對本題有透徹的把握。
程式碼:1236K,235MS
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int inf=1e5+10;
int head[inf],next[inf<<1],to[inf<<1],len[inf<<1],cnt;
int maxn[inf],siz[inf],G,subsiz;
bool vis[inf];
int dp[inf<<1],dep[inf<<1];//dp[]儲存到根節點的距離;dep[]是用來sort的,dep[0]表示dep陣列中元素的個數
int n,k,ans=0;
void init(void){
memset(vis,false,sizeof vis);
memset(head,0,sizeof head);
cnt=0;ans=0;
}
void addedge(int u,int v,int w){
to[++cnt]=v;len[cnt]=w;
next[cnt]=head[u];head[u]=cnt;
}
void getG(int u,int f){//找重心
siz[u]=1;maxn[u]=0;
for (int i=head[u];i;i=next[i]){
int v=to[i];if (v!=f && !vis[v]){
getG(v,u);
siz[u]+=siz[v];
maxn[u]=max(maxn[u],siz[u]);
}
}maxn[u]=max(maxn[u],subsiz-siz[u]);
G=(maxn[u]<maxn[G])?u:G;
}
void dfs(int u,int f){//dfs確定每個點到根節點的距離
dep[++dep[0]]=dp[u];
for (int i=head[u];i;i=next[i]){
int v=to[i];if (v!=f && !vis[v]){
dp[v]=dp[u]+len[i];
dfs(v,u);
}
}
}
int calc(int u,int inidep){//inidep是這一點相對於根節點的初始距離
dep[0]=0;
dp[u]=inidep;
dfs(u,0);
sort(dep+1,dep+1+dep[0]);
int sum=0;
for (int l=1,r=dep[0];l<r;){//計算合法點對數目
if (dep[l]+dep[r]<=k) {sum+=r-l;l++;}
else r--;
}
return sum;
}
void divide(int g){ //遞迴,找到重心並以重心為根節點進行計算,再對子樹遞迴處理
ans+=calc(g,0);
vis[g]=true;
for (int i=head[g];i;i=next[i]){
int v=to[i]; if (!vis[v]){
ans-=calc(v,len[i]);
maxn[0]=subsiz=siz[v];G=0;getG(v,0);
divide(G);
}
}
}
int main(){
while(scanf("%d%d",&n,&k)==2){
if (!n && !k) break;
init();
for (int i=1,u,v,w;i<n;i++){
scanf("%d%d%d",&u,&v,&w);
addedge(u,v,w);addedge(v,u,w);
}
subsiz=maxn[0]=n;G=0;getG(1,0);
divide(G);
printf("%d\n",ans);
}
return 0;
}