poj1741 tree(点分治)




Time Limit: 1000MS Memory Limit: 30000K
Total Submissions: 17730 Accepted: 5784

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

Source


[Submit]   [Go Back]   [Status]   [Discuss]


分析:
第一次写点分治

树上的路径无非两种:经过根结点的,不经过根结点的
对于不经过根结点的路径,我们可以把ta看做是经过某一棵子树的根结点的路径,换句话说,我们可以递归解决

所以我们现在的问题就是如何计算经过根结点的路径条数:
对于根结点进行一次dfs,求出每个结点的deep(到根结点的距离),并将其从小到大排序
避免重复,只需要求出其中deep[x]≤deep[y]且deep[x]+deep[y]≤m的有序对数(x,y)

计算方式挺简单:
用i表示左指针,j表示右指针,i从左向右遍历
如果deep[i]+deep[j]≤m,则点对(i,t)(i < t ≤ j)都符合题意,将j-i加入答案中,并且i++;否则j - -

然而这样还会重复计算在同一棵子树中的点对,
所以再进行下一步dfs之前需要减去重复部分

然而我们在递归处理子树的时候
每次不能固定选择root,而是以子树重心作为root去处理,这样能保证时间复杂度再O(nlog2n)以下


题目的做法很简单,我们来看一下具体的代码

Frist:找到整棵树的重心
void getroot(int now,int fa)
{
    f[now]=0;
    sz[now]=1;
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&vis[way[i].y]==0)       //vis表示这个连通块没有计算过 
        {
            getroot(way[i].y,now);
            sz[now]+=sz[way[i].y];
            f[now]=max(f[now],sz[way[i].y]);      //结点i连通的最大连通块 
        }
    f[now]=max(f[now],sum-sz[now]);               
    if (f[now]<f[root]) root=now;                 //树的重心 
}
Second:dfs

我们dfs一下这棵树,但是我按照dfs的顺序计算路径条数

void dfs(int now)
{
    deep[now]=0; 
    vis[now]=1;                        //以now为根结点的子树(连通块) 
    ans+=cal(now);
    for (int i=st[now];i;i=way[i].nxt) 
        if (!vis[way[i].y])
        {
            deep[way[i].y]=way[i].v;   //deep 
            ans-=cal(way[i].y);        //去重,把子树内部(不经过根结点)的路径减去 
            sum=sz[way[i].y];          //新连通块的大小 
            root=0;
            getroot(way[i].y,0);       //找到新连通块中的重心 
            dfs(root);                 
            //为了降低时间复杂度,我们对于每一个连通块都要从重心开始搜索 
        }
}

其中vis数组是一个很重要的数组,vis[i]代表的就是以i为根结点的子树是否计算过(计算经过根结点i的路径条数)

首先,我们先无脑加上这棵子树内的路径条数
之前就说过我们需要减去没有经过根结点的路径条数

之后就是递归处理的路径条数
但是如果无脑递归,就有可能因为树的形态退化成一条链而导致TLE

所以我们也需要在新的,没有计算过的连通块内找到重心,继续dfs

Third:计算子树内的路径条数
void getdeep(int now,int fa)
{
    d[++tt]=deep[now];
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&vis[way[i].y]==0)
        {
            deep[way[i].y]=deep[now]+way[i].v;
            getdeep(way[i].y,now);
        }
}

int cal(int now)
{
    tt=0;
    getdeep(now,0);
    sort(d+1,d+1+tt);
    int i=1,j=tt,ans=0;
    while (i<j)
    {
        if (d[i]+d[j]<=m) ans+=(j-i),i++;
        else j--;
    }
    return ans;
}

首先我们需要处理出仅仅在这棵子树中,根结点(实际上就是子树中的重心)到各个节点的距离

把各个结点按照从小到大排序,
用i表示左指针,j表示右指针,i从左向右遍历
如果deep[i]+deep[j]≤m,则点对(i,t)(i < t ≤ j)都符合题意,将j-i加入答案中,并且i++;否则j - -

tip

第一次写的点分治,不是很明白
如果距离等于k,cal可以改成:

int cal(int now)
{
    tt=0;
    getdeep(now,0);
    sort(d+1,d+1+tt);
    int l=1,r=tt,ans=0;
    while (l<r)
    {
        while (d[l]+d[r]>m&&l<r) r--;
        if (r<=l) break;
        int rr=r;
        while (d[l]+d[r]==m) {
            ans++;
            r--;
            if (r<=l) break;
        }
        r=rr;
        l++;
    }
    return ans;
}
//这里写代码片
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm> 

using namespace std;

const int N=10010;
int n,m;
struct node{
    int x,y,v,nxt;
};
node way[N<<1];
int st[N],tot=0,deep[N],f[N],ans,sum,root,sz[N],d[N],tt;
bool vis[N];

void add(int u,int w,int z)
{
    tot++;
    way[tot].x=u;way[tot].y=w;way[tot].v=z;way[tot].nxt=st[u];st[u]=tot;
}

void getroot(int now,int fa)
{
    f[now]=0;
    sz[now]=1;
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&vis[way[i].y]==0)       //vis表示这个连通块没有计算过 
        {
            getroot(way[i].y,now);
            sz[now]+=sz[way[i].y];
            f[now]=max(f[now],sz[way[i].y]);      //结点i连通的最大连通块 
        }
    f[now]=max(f[now],sum-sz[now]);               
    if (f[now]<f[root]) root=now;                 //树的重心 
}

void getdeep(int now,int fa)
{
    d[++tt]=deep[now];
    for (int i=st[now];i;i=way[i].nxt)
        if (way[i].y!=fa&&vis[way[i].y]==0)
        {
            deep[way[i].y]=deep[now]+way[i].v;
            getdeep(way[i].y,now);
        }
}

int cal(int now)
{
    tt=0;
    getdeep(now,0);
    sort(d+1,d+1+tt);
    int i=1,j=tt,ans=0;
    while (i<j)
    {
        if (d[i]+d[j]<=m) ans+=(j-i),i++;
        else j--;
    }
    return ans;
}

void dfs(int now)
{
    deep[now]=0; 
    vis[now]=1;                        //以now为根结点的子树(连通块) 
    ans+=cal(now);
    for (int i=st[now];i;i=way[i].nxt) 
        if (!vis[way[i].y])
        {
            deep[way[i].y]=way[i].v;   //deep 
            ans-=cal(way[i].y);        //去重,把子树内部(不经过根结点)的路径减去 
            sum=sz[way[i].y];          //新连通块的大小 
            root=0;
            getroot(way[i].y,0);       //找到新连通块中的重心 
            dfs(root);                 
            //为了降低时间复杂度,我们对于每一个连通块都要从重心开始搜索 
        }
}

int main()
{
    while (scanf("%d%d",&n,&m)&&n)
    {
        memset(st,0,sizeof(st)); tot=0;
        memset(vis,0,sizeof(vis));
        ans=0;
        for (int i=1;i<n;i++)
        {
            int u,w,z;
            scanf("%d%d%d",&u,&w,&z);
            add(u,w,z); add(w,u,z);
        }
        f[0]=N,sum=n;      //f[i]结点i连通的最大连通块,sum是当前连通块的大小 
        root=0;             
        getroot(1,0);      //找重心 
        dfs(root);          
        printf("%d\n",ans);
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值