[POJ1741]Tree(点分治+讲解)

时间:2021-09-29 04:23:24

题目:

我是超链接

题解:

这是一道点分治的模板啦,下见普及向如果还是不能理解
先求出子树中所有符合要求的点对数目,再减去在同一棵子树中的点对数目这句话的话
就是这个图咯
[POJ1741]Tree(点分治+讲解)
可以发现重心为1
先求出子树中所有符合要求的点对数目,如果3-2-1-2-4这条路符合要求的话
再减去在同一棵子树中的点对数目,诶这个3-2-4也符合啊?那么3-2-1-2-4这条路就不用算了,在统计子树的时候需要减去

那么具体如何统计一棵子树内的数量呢?
对于这道模板题来说,我们把过这个点的所有深度求出来,按照深度排序,设置两个指针l,r,如果最小的深度deep[l]+最大的深度deep[r]都超过了范围的话,说明这个deep[r]太大了啊,那么r–;如果满足要求的话,就把答案加上r-l,然后l++, O(nlogn)

下一个问题是如何减去3-2-1-2-4,我们在遍历2的时候,如果在有c[1-2]的基础上还是满足的边就是要减去的边!(因为减去c[1-2]就更满足了啊)

总的时间复杂度就是 O(nlog2n)

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define INF 1e9
using namespace std;
const int N=10005;
int tot,nxt[N*2],point[N],v[N*2],c[N*2],k,ans,n;
int root,sum,f[N],size[N],deep[N],d[N];bool vis[N];
void addline(int x,int y,int z)
{
    ++tot; nxt[tot]=point[x]; point[x]=tot; v[tot]=y; c[tot]=z;
    ++tot; nxt[tot]=point[y]; point[y]=tot; v[tot]=x; c[tot]=z;
}
void getroot(int x,int fa)
{
    f[x]=0; size[x]=1;
    for (int i=point[x];i;i=nxt[i])
      if (v[i]!=fa && !vis[v[i]])
      {
        getroot(v[i],x);
        size[x]+=size[v[i]];
        f[x]=max(f[x],size[v[i]]);
      }
    f[x]=max(f[x],sum-f[x]);
    if (f[x]<f[root]) root=x;
}
void getdeep(int x,int fa)
{
    deep[++deep[0]]=d[x];
    for (int i=point[x];i;i=nxt[i])
      if (v[i]!=fa && !vis[v[i]])
      {
        d[v[i]]=d[x]+c[i];
        getdeep(v[i],x);
      }
}
int calc(int x,int now)
{
    d[x]=now; deep[0]=0;
    getdeep(x,0);
    sort(deep+1,deep+deep[0]+1);
    int l=1,r=deep[0],t=0;
    while (l<r)
      if(deep[l]+deep[r]<=k) t+=r-l,l++;
      else r--;
    return t;
}
void work(int x)
{
    ans+=calc(x,0);
    vis[x]=1;
    for (int i=point[x];i;i=nxt[i])
      if (!vis[v[i]])
      {
        ans-=calc(v[i],c[i]);
        sum=size[v[i]]; root=0;
        getroot(v[i],x);
        work(root);
      }
}
int main()
{
    while (scanf("%d%d",&n,&k) && n)
    {
        ans=0;tot=0;memset(point,0,sizeof(point));
        memset(vis,0,sizeof(vis));
        for (int i=1;i<n;i++)
        {
            int x,y,z;scanf("%d%d%d",&x,&y,&z);
            addline(x,y,z);
        }
        sum=n; f[0]=INF; root=0; getroot(1,0);
        work(root); printf("%d\n",ans);
    }   
}

普及向

这里说一下点分治吧——有些引自优秀的学姐
点分治是树上的分治操作,对于当前处理的一棵树,从中选择一个点,这个点能够把这棵树分成尽量平均的好多棵更小的子树。然后就相当于有了很多更小的子问题,就可以分治下去求解。

点分治保证时间复杂度的关键就是选择的点能够把这棵树分得尽量平均,也就是分完了以后规模最大的那棵子树是所有划分方案中最小的。这样的话设原来的树规模为size,分完了以后规模最大的子树不会超过size/2。也就是说每次分治下去的子问题规模至少是砍去一半的。那么也就是说这样递归分治的层数不会超过log层,而因为每层处理的子树互不相交,每层总的复杂度是O(n)的。如果不考虑计算过程中额外的复杂度,点分治的总复杂度就是O(nlogn)的

这个很关键的点,大家都想到了吧?重心

那么点分治的大体流程就出来了!
①找出这颗树的重心。
②统计经过这个重心的答案
③用重心把树割开
④对每个“小树”做同样的事

注意处理③的时候我们不能经过过这棵子树的重心啊,那我们就给这个重心打个标记(vis),不准通过就好了
那么点分治的难点就在于设计②过程啊

用到了点分治中常见的补集转化思想,也就是要求路径两个端点不在同一棵子树中的点对数目的时候,先求出子树中所有符合要求的点对数目,再减去在同一棵子树中的点对数目。而后者可以在枚举子树的时候顺便求出来。

这类题目需要发现题目中要求的路径的一些性质,比如可以排序以后利用单调性或者用排列组合的原理求解之类的。

UPD-12.4
点分治要告一段落啦,还有几道比较难的题目以后再练啦

[Codeforces716E]Digit Tree

BZOJ3784树上的路径

BZOJ3648寝室管理