POJ-1741 (点分治模板)

73 篇文章 0 订阅

题目

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

分析

  • 题目大意就是给一棵树,每条边有一个长度,两个点之间的距离就是连接它俩的边的长度之和。求出这棵树中满足距离不大于 K 的点对个数。
  • 这基本上就是树的点分治模板题,我也是第一次打树的分治,主要有以下几个步骤:
    • 对于一棵子树的分治,首先求出它的重心, 我们目的是对这棵子树讨论所有经过重心的路径。(重心的作用是保证时间复杂度)
    • 之后,以此重心为根节点,对这棵树进行一遍深搜,得出每个节点到重心的距离dis[]
    • 之后,这棵子树中相连的路径会经过重心且对答案有贡献的点对(i,j) (i<j)就会是这样: dis[i]+dis[j] <= K 且在去除重心后,i 与 j 不在同一个联通块里
  • 不过显然要满足“不在同一个联通块里”这个条件有点突兀,于是就有了一个小技巧:
    • 先不管在不在一个联通块这个条件,算出当前这棵树的符合路径数,之后再将得出的个数减去 以重心的儿子节点为根的子树内 的 点对 路径距离(经过重心)小于等于K的个数,就行了。
    • (可能这句话有点绕,不会的话看看程序再理解理解,应该也是能领悟到的)
  • 之后,再分治一下现在这棵子树,步骤同上。

  • 一棵子树的重心其实就是你要找到一个点,使得删掉这个点后,这棵子树剩下最大(节点个数最多)联通块最小。

  • 为什么每次都要算一个重心呢?你想想,要是那一棵子树刚好是一条链(就是一整条下来),而默认的根节点又在链的端点上,那么时间不就退化到了还不如暴力的程度了?于是我们求个重心,把时间复杂度保证在了 log 级别里面,就很多了。

程序

#include <cstdio>
#include <algorithm>
#define Max 20010
#define add(u,v,w) (To[++num]=Head[u],Head[u]=num,V[num]=v,W[num]=w)
#define For(x) for (int h=Head[x],o=V[h]; h; h=To[h],o=V[h])
#define Input for (int i=1,u,v,w; i<N; i++) scanf("%d%d%d",&u,&v,&w),add(u,v,w),add(v,u,w)
using namespace std;
int N,K,root,num,ans,cnt,mins,Head[Max],To[Max],V[Max],W[Max],siz[Max],dis[Max],f[Max];
/*
    程序中部分变量说明
    dis[i]  所有路径到 重心 的长度
    siz[i]  以 i 为根节点的子树的大小(当前重心下)
    f[i]    i 是否还存在(分治完一次后就把重心删掉了)
    cnt     记录 dis 的个数(即路径个数)
    root    当前子树的重心
    maxs    当前讨论的点所有子树大小中最大值(并不是全局变量,是尝试每个重心时重新开的一个变量)
    mins    讨论过的点的子树大小中最大的最小值(是全局变量,用来确定哪个才是重心)
*/

int get_size(int x,int fa){     //返回以 x 为根的子树大小,其中 x 父节点为 fa 
    siz[x]=1;
    For(x) if (o!=fa && !f[o])
        siz[x]+=get_size(o,x);
    return siz[x];
}

void get_dis(int x,int d,int fa){   //x 到重心的长度为 d,之后继续 dfs 
    dis[++cnt]=d;
    For(x) if (o!=fa && !f[o])
        get_dis(o,d+W[h],x);
    return;
}

void dfs_root(int x,int tot,int fa) {
    //求目标子树的重心(要求除去 x 点时,它的 maxs 值最小,那么 x 就是这棵子树的重心了),其中 tot 是这棵子树的总大小(节点个数) 
    int maxs=tot-siz[x];    //这棵子树中x 父亲那一支先赋给 maxs 
    For(x) if (o!=fa && !f[o]){
        maxs=max(maxs,siz[o]);
        dfs_root(o,tot,x);
    }
    if (maxs<mins){
        mins=maxs;
        root=x;
    }
    return;
}

int work(int x,int d) {
    //返回以 x 为根的子树内长度小于等于 K 的路径数(两个端点都在子树内) 
    //其实 d 在这里用处只有一个,是在做减法时方便把重心的儿子节点的 dis 先弄好,你也可以在分治的时候弄,不过就稍微有点麻烦了 
    cnt=0;
    get_dis(x,d,0);
    sort(dis+1,dis+cnt+1);
    int daan=0,i=1,j=cnt; 
    while (i<j){
        while (i<j && dis[i]+dis[j]>K) j--;
        daan+=j-i;  //相当于选一条路径 i,另一条可以为 [i+1,j] 里任意一条路径,这样得到的两个点之间长度(经过重心的那条路径)肯定是小于等于 K 的 
        i++;
    }
    return daan;
}

void dfs(int x){    //以 x 为重心分治一下 
    cnt=0;
    mins=Max;
    get_size(x,0);
    dfs_root(x,siz[x],0);
    ans+=work(root,0);
    f[root]=1;
    For(root) if (!f[o]){       //注意这里是以重心开始 
        ans-=work(o,W[h]);      //注意,这里 dis[o] 要先赋成 W[h](即它到重心的距离) 
        dfs(o);
    }
    return;
}

int main(){
    while(scanf("%d%d",&N,&K)!=EOF && N && K){
        Input;
        dfs(1);
        printf("%d\n",ans);

        num=ans=0;
        for (int i=1; i<=N; i++) Head[i]=f[i]=dis[i]=0;
    }
    return 0;
}

时间复杂度

  • 我们注意到,对于一棵大小为 n 的树,重心选好后,它的分支大小都是不大于 n2  的,那么每次分治后,联通块的大小会至少减少一半,因此最多递归分治 Log2n 层,每次分治都会把整棵树的点都讨论一次,于是总的时间复杂度就是:
    nlog2n   

提示

  • 开始我写的时候sort的边界弄错了,结果一直WA,要注意一下这些细节问题。
  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值