您的位置:首页 > 其它

poj 1741 Tree 点分治

2016-10-14 08:53 417 查看
//  poj 1741 Tree 点分治

// 题目链接:

//      http://poj.org/problem?id=1741 
// 题目大意:

//      给定一棵N(1<=N<=10000)个结点的带权树,定义dist(u,v)为u,v两点间的最短路径长度,
//  路径的长度定义为路径上所有边的权和。再给定一个K(1<=K<=1e9),如果对于不同的两个
//  结点a,b,如果满足dist(a,b)<=K,则称(a,b)为合法点对。求合法点对个数。

// 解题思路:

//      点分治.首先我们将树转化成有根树,这样点对有两种情况:
//  1)在同一个子树内
//  2)路径经过根节点

//      对于第一种情况,我们进行递归处理,可以转化成第二种情况.
//      对于第二种情况.我们设belong[i] = x(x为根节点的某个儿子,i是在x为根节点的子树内)
//  这样我们要求的就是
//      dist[i] + dist[j] <= K(belong[i] != belong[j])(跨根节点)
//  =   dist[i] + dist[j] <= K(i != j) - (dist[i] + dist[j] <= K(belong[i] == belong[j])
//  这样,对于前者,我们求以root为根的节点的ans[root],对于后者,答案是root的儿子节点的ans[v]
//  的和.最后结果是ans += ans[root] - (ans[v1] + ans[v2] + ... + ans[vk])(v1..vk为root的
//  儿子).

//      最后,就是这个root如何选择,如果每次暴力以每个节点为root,这样无疑是会超时的.树分治
//  每次以树的重心为root,这样子树的点就会减小一般,所以是log(n),求出点对数也很好做,一遍快排
//  然后O(n)复杂度可以算出.

//      最后总结,求重心,以重心递归,以每个重心算出当前所有点到重心的距离dis,排序算出点对做累
//  加再对重心的每个儿子算出点对做累减.

//  感悟:

//      第一次接触点分治,看了09年漆子超大神的论文,以及各位大神的题解博客,才勉强弄懂,有种兴奋
//  对算法的奥妙有着无穷的神往,不多说啦,加油~~~ ^ _ ^

//  参考材料:

//      http://wenku.baidu.com/view/e087065f804d2b160b4ec0b5.html###(漆子超论文) //      http://blog.sina.com.cn/s/blog_6d5aa19a0100o73m.html(整体思路) //      http://blog.csdn.net/woshi250hua/article/details/7723400(明白为何做减法) //      http://blog.csdn.net/sdj222555/article/details/7893862(参考具体实现) 
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int MAXN = 10000 + 8;

int num;
int head[MAXN];
int siz[MAXN];
int vis[MAXN];
int dis[MAXN];
int root;
int n;
int k;
int mins;
int cnt;
int ans = 0;

struct Edge{
int to;
int w;
int next;

Edge(){
}

Edge(int to,int w,int next):to(to),w(w),next(next){
}
}edges[MAXN << 1];

void init(){
memset(head,-1,sizeof(head));
memset(vis,0,sizeof(vis));
num = 0;
ans = 0;
}

void add_edges(int u,int v,int w){
edges[num] = Edge(v,w,head[u]);
head[u] = num++;
}

void input(){
for (int i = 1;i < n;i ++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edges(u,v,w);
add_edges(v,u,w);
}
}

void dfssize(int u,int fa){ // 求size[u](以u为根节点的子树节点数目)
siz[u] = 1;

for (int i = head[u];i != -1;i = edges[i].next){
int v = edges[i].to;
if (v == fa || vis[v])
continue;
dfssize(v,u);
siz[u] += siz[v];
}
}

void dfsroot(int u,int fa,int tot){ // 求重心(找到除去这个节点,各子树中最大节点数最小)
int maxs = tot - siz[u];

for (int i = head[u];i != -1;i = edges[i].next){
int& v = edges[i].to;

if (v == fa || vis[v])
continue;

dfsroot(v,u,tot);

maxs = max(maxs,siz[v]);

}

if (maxs < mins){
mins = maxs;
root = u;
}
}

void dfsdis(int u,int fa,int d){ // 求各个点到重心的距离
dis[cnt++] = d;
for (int i = head[u];i != -1;i = edges[i].next){
int& v = edges[i].to;
if (v == fa || vis[v])
continue;

dfsdis(v,u,d + edges[i].w);
}
}

int cal(int u,int d){
cnt = 0;
dfsdis(u,0,d);
sort(dis,dis + cnt);
int i = 0;
int j = cnt - 1;
int res = 0;
while(i < j){ // O(n)经典算法
while(i < j && dis[i] + dis[j] > k) j--;

res += j - i;

i ++;
}
return res;
}

void dfs(int u){ // 以重心分治
cnt = 0;
mins = MAXN;
dfssize(u,0);
dfsroot(u,0,siz[u]);
ans += cal(root,0);
vis[root] = 1;

for (int i = head[root];i != -1;i = edges[i].next){
int& v = edges[i].to;

if (!vis[v]){
ans -= cal(v,edges[i].w);   //  这里减是很微妙的,在参考材料中的第三
dfs(v);                     //  中,有一些介绍,"比如1是根,1<-2<-3<-4,
//  dis(4,1) = 10,dis(3,1) = 5,k = 20,
//  那么dis(4,1)和dis(3,1)也合法,这显然
//  不符合逻辑",这句话的理解,让我有些疑惑
}                               //  我想应该这是重复计算了因为dis(3,4)会在
//  3为根的子树中计算一次,如果在1为根子树
//  中再次计算,就会重复

}

}

int main(){
//freopen("1.txt","r",stdin);
while(scanf("%d%d",&n,&k)!=EOF && n && k){
init();

input();

dfs(1);
printf("%d\n",ans);

}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  poj 1741 Tree 点分治