点分治模板

点分治模板

点分治:

这里统一地,只讲一下主要思想,各种细节留给参考文章

点分治的主要是处理树上的路径问题,如果用暴力做法求解会有很高的时间复杂度(O(n^2)-O(n^3))的样子。而如果使用点分治,就可以将复杂度降为(O(nlog_2n))。主要思路是将一棵树分解成若干子树,在分解成子树的子树,递归地求解出部分答案(分),最后在递归回去时合并答案(治)。至于怎么分解树,这里是要找到一个合理的根。使得这个跟的最大子树能够最小,这样的根叫做树的重心。举个栗子:一条链,是从端点处一个一个处理到头快呢,还是从中心往两边处理快?是中心处理快((O(log_2n))),而不是端点(O(n))。因此我们需要一个找重心的过程。找到了重心就可以递归地处理子树了。这个过程就是分的过程,我们找到了重心后,在以重心为根的树的子树中再一次找重心,一直分下去。治的过程是对每个重心统计答案的过程,这个过程可能需要求出点到重心的距离,也可能用容斥原理去重,而且可能用到二分加速找答案。最后就在返回后得到了结果。

大概思路是这样(我也不太清晰),具体请参看参考文章

这里先放一道模板题:P3806 【模板】点分治1的代码,代码里面就包含了基本上最基本点分治的内容。

代码:

#include <iostream>
#include <algorithm>
#define INF 0x3f3f3f3f
#define max_n 200005
using namespace std;
int n,m;//n为节点数,m为询问次数
int ask[max_n];//记录询问距离
//链式前向星
int cnt = 0;
int head[max_n];
struct edge
{
    int v;
    int w;
    int nxt;
}e[max_n<<1];
void add(int u,int v,int w)
{
    ++cnt;
    e[cnt].v=v;
    e[cnt].nxt=head[u];
    head[u]=cnt;
    e[cnt].w=w;//多了边上的权值
}
int rt,ms;//rt为重心,ms是最小 最大子树的大小
int t;//t为dis数组计数量
int Size;//当前整棵树的大小
int sz[max_n];//sz[i]表示以i为根的子树大小
int mson[max_n];//mson[i]表示以i的最大子树的大小
int dis[max_n];//到某节点的距离数组
bool visit[max_n];//标记是否分治过
int ans[max_n];//记录答案
//二分用到的结构数组
int tt;
struct asd
{
    int x;//大小
    int y;//次数
}arr[max_n];
//求重心函数
void get_root(int u,int from)
{
    sz[u]=1;mson[u]=0;//开始时以u为根的子树大小为1,u的最大子树为0
    for(int i=head[u];i;i=e[i].nxt)//遍历与u相连边
    {
        int v=e[i].v;
        if(visit[v]||v==from) continue;//防止重复遍历
        get_root(v,u);
        sz[u]+=sz[v];//更新以u为根的子树大小
        mson[u]=max(mson[u],sz[v]);//更新u的最大子树
    }
    if(Size-sz[u]>mson[u]) mson[u]=Size-sz[u];//看是否另一半子树更大
    if(ms>mson[u]) ms=mson[u],rt=u;//更新最小的最大子树和重心
}
//求距离函数
void get_dis(int u,int from,int w)
{//t一开始初始化为0
    dis[++t] = w;
    for(int i=head[u];i;i=e[i].nxt)
    {
        int v=e[i].v;
        if(visit[v]||v==from) continue;
        get_dis(v,u,w+e[i].w);
    }
}
//求解答案函数
void conquer(int x,int y,int id)//x表示get_dis的起始点,y表示节点x到目标点的距离,id为+/-1
{
    t=0;//记得初始化dis计数量
    tt=0;//记得初始化二分用到的结够数组计数量
    get_dis(x,0,y);//计算x到目标点的距离,注意这里y可能因为去重,本身传进来就不是0
    sort(dis+1,dis+t+1);//从小到大排序,距离相同的都紧挨在一起
    dis[0]=-233;//重要
    for(int i = 1;i<=t;i++)
    {
        if(dis[i]!=dis[i-1]) arr[++tt].x=dis[i],arr[tt].y=1;//如果距离和上一个不一样,新开一个
        else arr[tt].y++;//否则就在旧的基础上加一
    }
    for(int i = 1;i<=m;i++)
    {
        if(ask[i]%2==0)//看有没有到x距离刚好是k/2的点
        {
            for(int j=1;j<=tt;j++)
            {
                if(arr[j].x==ask[i]/2)
                {
                    ans[i]+=(arr[j].y-1)*arr[j].y/2*id;
                }
            }
        }
        for(int j=1;j<=tt&&arr[j].x<ask[i]/2;j++)//如果还有可以枚举的,并且距离小于k/2的点,继续
        {//不用枚举距离大于k/2的
            int l=j+1;int r=tt;
            while(l<=r)//二分查找距离和为k的点
            {
                int mid=(l+r)>>1;
                if(arr[mid].x+arr[j].x==ask[i])
                {
                    ans[i]+=arr[j].y*arr[mid].y*id;//统计答案
                    break;
                }
                if(arr[mid].x+arr[j].x<ask[i])
                {
                    l = mid+1;
                }
                else
                {
                    r = mid-1;
                }
            }
        }
    }
}
//分函数
void divide(int u,int ssize)
{
    visit[u]=true;//当前节点已分治
    conquer(u,0,1);//计算以u为重心的所有组合
    for(int i = head[u];i;i=e[i].nxt)
    {
        int v=e[i].v;
        if(visit[v]) continue;//已分治过的不用再分治
        conquer(v,e[i].w,-1);//所有组合减去不合法的组合,注意第二个参数距离已经加上了u到z的距离
        ms=INF;rt=0;//每一次求重心都要初始化这两个值
        Size=sz[v]<sz[u]?sz[v]:ssize-sz[u];/*由于dfs的起点不是重心原因,最终算出来的不是真正重心的子树大小,这样做能以重心为参考系,算出以重心为根的子树大小,具体解释见参考文章*/
        get_root(v,u);//求出子树的重心
        divide(v,Size);//分治子树
    }
}
int main()
{
    cin >> n >> m;
    for(int i = 1;i<n;i++)
    {
        int u,v,w;
        cin >> u >> v >> w;
        add(u,v,w);
        add(v,u,w);
    }
    for(int i = 1;i<=m;i++)
    {
        cin >> ask[i];
    }
    rt=0;ms=INF;
    get_root(1,0);
    divide(rt,n);
    for(int i = 1;i<=m;i++)
    {
        if(ans[i]>0)
        {
            cout << "AYE" << endl;
        }
        else
        {
            cout << "NAY" << endl;
        }
    }
    return 0;
}

参考文章:

Hypoc_,树上点分治详解【入门向】,https://blog.csdn.net/a_forever_dream/article/details/81778649(真心讲的细啊讲的好)

守望,点分治略解,https://www.luogu.org/blog/user9012/dian-fen-zhi-lve-xie(点分治的大致思想和细节)

gigo_64,点分治入门,https://blog.csdn.net/qq_42835815/article/details/87067143(与第一个比要简略一点,但思路还是很清晰的)

我大概也只能看得懂入门的文章了吧(/ω\)

原文地址:https://www.cnblogs.com/zhanhonhao/p/11313791.html