poj 1741 Tree 点分治

23 篇文章 0 订阅
1 篇文章 0 订阅
//  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;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值