1. 程式人生 > >POJ 1741 Tree, 樹的重心, 樹分治, 點分治

POJ 1741 Tree, 樹的重心, 樹分治, 點分治

最近在學習樹的分治,算是比較難,而且程式碼量比較大的一塊。隨便拿一道題來就有上百行,故寫一篇文章來總結一下這方面的框架。

POJ這一題應該算是樹分治的入門題,順便用這一題來詳細說明樹分治的一些具體內容。

Tree Time Limit: 1000MS Memory Limit: 30000K

Description

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 input contains several test cases. The first line of each test case contains two integers n, k. (n<=10000) The following n-1 lines each contains three integers u,v,l, which means there is an edge between node u and v of length l. 
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;
}