• 点分治模板


    点分治模板

    点分治:

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

    点分治的主要是处理树上的路径问题,如果用暴力做法求解会有很高的时间复杂度(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(与第一个比要简略一点,但思路还是很清晰的)

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

  • 相关阅读:
    Linux系统信息查看
    批处理Dos命令
    python语法31[函数]
    python基础31[数据结构list+tuple+set+dictionary]
    nmake使用
    linux间文件拷贝
    linux文件同步工具Unison的使用
    python语法31[基本数据类型和流程控制]
    远程桌面管理工具比较
    WindowsBatch与LinuxShell比较[batchfile之for命令详解]
  • 原文地址:https://www.cnblogs.com/zhanhonhao/p/11313791.html
Copyright © 2020-2023  润新知