点分治学习笔记

银月城传送门

集训day9上午各种神仙分治真心听不懂,自己颓着把点分治学了,简单(并不)写一下再加深一下理解吧。


一、点分治是个啥
点分治是通常用来处理树上路径统计问题的一种分治方法。尤其适用于大规模的数据处理。时间复杂度最高为O(nlogn)。
例如求树上两点间距离,点分治就远优于dfs求两点距离(O(n^3)复杂度)

二、点分治的实现原理
以求树上两点路径为例子,让我们构想一棵鼠:
点分治学习笔记

姑且将其命名为小捞鼠。

考虑路径问题,假设一条满足条件的路径经过点 1 ,那么会出现两种情况:

1.这条路径在 x 的一个子鼠里(以 1 为端点)

2.在 1 的两个不同的子鼠里。

这时候一个想法就会是从根节点出发进行dfs,分别处理紫薯子树紫鼠子鼠的答案。

等等,都说了路径经过点 1 可能产生两种情况,莫非是要分类讨论?小捞鼠这么友善肯定不会让我们分类讨论的。

并不会,对于情况 2,我们可以果断直接分治,假设要求出点 4 到点 7 的路径,我们可以分别处理 4 和 7 ,用dis[]表示当前结点到根节点 1 的路径长度, 则 4 到 7 的路径长即为dis[4]+dis[7]。

再看情况1,假设我们要求点 4 到点 5 的路径,则既然它们之间的路径包含在 1 的某个子树内, 那么我们就可以找到这棵子鼠的根,然后按第一类情况处理。
于是分治的思想就形成了。

三、点分治的具体实现
上文提到,我们的想法是“从根节点出发进行dfs,分别处理紫薯子树紫鼠子鼠的答案。
那么问题又来了,我们该从哪个根节点出发呢?
考虑一下类比,若是在链上进行分治,想必是从中间分为最佳,为什么呢?因为这样分可以使各部分尽量平均,递归效率就会最大化。
那么我们只要找到一个节点,使该节点被删除后形成的各子鼠肥大啊不大小尽量平均。因为这样才能最大程度减小这棵鼠在扣篮时因为重心不稳而滑倒的可能!
重心概念应运而生。
定义:找到一个点,其所有的子树中最大的子鼠节点数最少,那么这个点就是这棵鼠的重心,删去重心后,生成的多棵鼠尽可能平衡

以下为求重心代码,帮助理解:(sum代表以父节点为根的子鼠大小)

void getroot(int id,int fa)
{
     size[id]=1;//初始化鼠大小
     maxp[id]=0;//记录最大子鼠大小
     for(int i=head[id];i;i=nxt[i])//遍历连边
    { 
        if(to[i]==fa||vis[to[i]])//如果回到父节点或已查询就跳过
        continue;
        getroot(to[i],id);//递归
        size[id]+=size[to[i]];//加上子鼠大小
        maxp[id]=max(size[to[i]],maxp[id]);//更新最肥子鼠
    }
    maxp[id]=max(maxp[id],sum-size[id]);//与父节点的另一子鼠比较
    if(maxp[id]<maxp[root])//若该子鼠小,更新重心
    root=id;
}

于是来到了最重要的分治部分,这个部分有点难懂我要结合一下真正的题干:


题目描述

给定一棵有n个点的树

询问树上距离为k的点对是否存在。

(多次询问&&可离线)


求的不是某条路径,而是从所有可能的路径中统计答案。

这好办啊,先离线所有的询问,将k存储在一个query数组里,枚举所有可能路径,每次扫一遍query数组,若路径长度=k就标记该次询问,最后输出标记就可以了。

那怎么用分治实现呢?

从重心向下枚举子鼠根节点,根节点又会形成子鼠为v1,v2……vt, 我们从第一个子鼠v1开始,求出每棵子鼠的dis[]

假设当前处理的子鼠为vi, 对离线记录的每个询问, 枚举v1到vi-1的dis与当前子鼠的dis的和, 若相等就标记

路径加和后将这棵子鼠的dis与前面的dis一起保存于一个数组, 当这个根节点的子鼠查询完后清空这个数组

然后对其他子鼠进行分治,可以保证所有路径都被枚举过。

END

有讲的不清楚的地方随时私信本蒟蒻,下方的代码给出了详细注释


die码如下:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define inf  10000000
#define maxn 100010
using namespace std;
int n,m;
int cnt,nxt[maxn<<1],to[maxn<<1],head[maxn],val[maxn<<1];//链前存边用的
int maxp[maxn],size[maxn],dis[maxn],rem[maxn],tot;//maxp求重心时保存最大子鼠
int vis[maxn],test[inf],ju[inf],q[maxn];//vis为标记数组,test存储每个询问的结果,ju[i]表示到根距离为i的路径是否存在
int query[1010];//存储询问
int sum,root;

void addedge(int x,int y,int v)
{
    nxt[++cnt]=head[x];
    to[cnt]=y;
     head[x]=cnt;
     val[cnt]=v;
}
 
void getroot(int id,int fa)//求重心,详细注释见上文
{
     size[id]=1;
     maxp[id]=0;
     for(int i=head[id];i;i=nxt[i])
    {
        if(to[i]==fa||vis[to[i]])
        continue;
        getroot(to[i],id);
        size[id]+=size[to[i]];
        maxp[id]=max(size[to[i]],maxp[id]);
    }
    maxp[id]=max(maxp[id],sum-size[id]);
    if(maxp[id]<maxp[root])
    root=id;
}

void getdis(int id,int fa)
{
    rem[++rem[0]]=dis[id];
    for(int i=head[id];i;i=nxt[i])
    {
        if(to[i]==fa||vis[to[i]])
        continue;
        dis[to[i]]=dis[id]+val[i];
        getdis(id,to[i]);//递归求dis
    }
}

void cacl(int id)
{
    int now=0;
    for(int i=head[id];i;i=nxt[i])
    {
        if(vis[to[i]])
        continue;
        rem[0]=0;
        dis[to[i]]=val[i];
        getdis(to[i],id);//求solve函数的子鼠的所有子鼠的dis值
        for(int j=rem[0];j;j--)
            for(int k=1;k<=m;++k)
                if(query[k]>=rem[j])
                    test[k]|=ju[query[k]-rem[j]];//与询问比较,记录答案
        for(int j=rem[0];j;j--)
        {
            q[++now]=rem[j];
            ju[rem[j]]=1;
        }
    }
    for(int i=1;i<=now;i++)
        ju[q[i]]=0;//为下一棵子鼠清空
}

void solve(int id)
{
    vis[id]=ju[0]=1;
    cacl(id);//处理该子鼠
    for(int i=head[id];i;i=nxt[i])//分治子鼠
    {
        if(vis[to[i]])
        continue;
        sum=size[to[i]];
        maxp[root=0]=inf;
        getroot(to[i],0);//找下个鼠的重心
        solve(root);//处理下个子鼠
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<n;i++)
    {
        int u,v,dis;
        scanf("%d%d%d",&u,&v,&dis);
        addedge(u,v,dis);
        addedge(v,u,dis);
    }
    for(int i=1;i<=m;i++)
    scanf("%d",&query[i]);
    maxp[root]=sum=n;//这一行和下一行,找整棵鼠的重心 ,我的鼠终于不会扣篮滑倒了
    getroot(1,0);
    solve(root);//操作
    for(int i=1;i<=m;i++)
        if(test[i])//愉快地输出答案
              printf("AYE ");
          else
               printf("NAY ");
    return 0;
}