Tree POJ - 1741(点分治)

学习博客:POJ-1741 (点分治模板)_Jacky_50的博客-CSDN博客

算法分析课设(四)采用分治法求树中任意两点之间距离小于K的点对及路径_Hillbox的博客-CSDN博客m

思想:

点分治是树分治的一种,树分治分为点分治和边分治

树的重心,也叫树的质心。即树的一个结点,把它删掉后的所有子树的最大结点数(相比于删掉其他结点)最小。换句话说,删除这个节点后最大连通块(一定是树)的节点数最少。

树的重心使得树尽可能的平衡,对于降低时间复杂度有帮助

求树的重心的复杂度为O(n)

邻接表:邻接表_黑色吊椅的博客-CSDN博客_邻接表

主要看图,前面的数组表示点集,后面链接的数组表示边集。

参数d的作用:dis[i]+dis[j] <= K 且在去除重心后,i 与 j 不在同一个联通块里。

work的定义为:按照给定的根节点,所有以根节点为中间节点的dis[i]+dis[j] <= K的数量,但是经过分治后,发现分治后的子树有所重复

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
///定义邻接表去存储树
#define add(u,v,w) (To[++num]=Head[u],Head[u]=num,V[num]=v,W[num]=w)///To表示边集,里面记录的是相同起点的上一条边;head表示点集,里面记录的是相同起点的最后一条边的编号;V代表边集的末端节点
#define For(x) for (int h=Head[x],o=V[h]; h; h=To[h],o=V[h])///用来遍历,h代表一条边的边号,默认一个顶点对应刚加入的第一条边的上一条边编号为0,表示没有,
#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)///为什么是<N,因为树的边有N-1条
const int v=1e5+10;
using namespace std;
int mask[v];///用来标记当前节点是否被删除
int To[v*2],Head[v],V[v*2],W[v*2];
int siz[v],dis[v],cnt,mins,root;
int N,K,num;
int get_size(int x,int fa)///返回的是包括根节点的,整棵树的大小
{
    siz[x]=1;
    For(x) if (o!=fa && !mask[o])///如果到了叶子节点,就不会进for循环,siz[x]也便只能返回1,mask[o]确保只在分治的子树当中进行操作,为什么需要fa,因为fa还没有被mask标记
        siz[x]+=get_size(o,x);
    return siz[x];
}

void get_dis(int x,int d,int fa)//x 到重心的长度为 d,之后继续 dfs ,d应该相当于一个负重
{
    dis[++cnt]=d;///这里d的作用是造出一棵当前子树分割前的树
    For(x) if (o!=fa && !mask[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 && !mask[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;
}

int dfs(int x)///x的作用是传入一棵树的一个起点,把这个起点先当做根进行遍历;这个函数的作用是求原问题的答案,在这个函数中体现了分治
{   ///在子树合并时,需要求得以经过合并的节点为中间节点的答案数量,但是涉及到了重复这样一种问题,
    ///也就是此合并节点不是联通块的直接父节点而是间接父节点,会导致加上直接父节点到合并节点的路径长度也有可能满足<=k这样一个条件,导致重复
    mins=0x7fffffff;///对于每一个节点,所有子树中节点的最大值,在每一个节点中最小,用来记录最小的所有子树中节点的最大值
    get_size(x,0);///0表示当前节点为根节点,siz[x]表示以x为根节点,没有被mask标记节点删除的树的节点个数
    dfs_root(x,siz[x],0);///求重心
    int ans=work(root,0);
    mask[root]=1;
    For(root) if (!mask[o])///为什么必须要mask,因为fa可能会再次遍历到,如果没有标记
    {       //注意这里是以重心开始
     ans-=work(o,W[h]);      //注意,这里 dis[o] 要先赋成 W[h](即它到重心的距离)
     ans+=dfs(o);
    }
    return ans;
} ///还有一种没有返回值的写法
int main()
{

    while(scanf("%d%d",&N,&K)!=EOF && N && K)///节点的编号是从1开始的
    {
        Input;
        int ans=dfs(1);
        printf("%d\n",ans);
        num=0;
        for (int i=1; i<=N; i++) Head[i]=mask[i]=dis[i]=0;
    }
    return 0;
}

最后其时间复杂度我认为:O(n*(logn)^2)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值