• 树分治总结


    点分树(治)
    点分用于处理树上路径/信息问题。它是分治的拓展版本。
    实际上,如果树是链,则分治法每次将序列从正中间劈开,正中间就是重心。
    如果计算跨过一个点的路径的时间复杂度较小,则可以考虑点分。
    点分治是每次取整棵树的重心后再分治,这样子可以保证时间复杂度不超过$nlogn*计算复杂度$
    对于路径题目,计算跨过重心的有2种方式:
    1.容斥:计算子树间(有可能和自身组合)的答案,然后在做子树时容斥一下。
    2.动态计算:每次插入一棵子树,在另一颗子树上查询当前的子树能对答案产生多少贡献。
    这样子就不用容斥。
    实际上,这2种方法都有用处,要具体题目具体分析。
    动态计算法可以用于要求的信息没有逆元的题目上。
    如果把分治每个子树的重心向当前重心连边,则得到点分树,高度不超过log_2n
    点分树的性质是对于每个点,它子树上的点互相组成的路径上包含这个点。
    这个性质可以用来解决一些带查询的点分治问题(如scoi2016 幸运数字)
    如果每个点维护它的子树信息,则可以在一些问题上支持修改,如震波。
    这种题目要先从点分的角度思考,然后再考虑是否能动态点分。
    边分治:每次选择一条边,然后找到两边节点数最相近的边。
    这样子对于点分的好处是每次只要处理两边,但是这样子也有局限性。
    实际上,如果原图是菊花树的话,这样子就会被卡成n^2级别,需要加虚点让树更加平衡。
    加点的方法类似kruskal重构树。
    也有边分树的概念。边分树是:一个点向分出它的边连边,这样子会连出一个二叉树。
    二叉树可以使用类似线段树合并的方式维护,也就是边分树合并。
    总结:点/边分树实际上就是分治,枚举重心使时间复杂度不会退化。
    实际上,点分/边分治的形态可以把它和其他知识点联系起来,这样子就产生了[CTSC2018]暴力写挂,[WC2014]紫荆花之恋两道题。
    例题:
    [集训队]聪聪可可
    这道题可以直接使用点分治,按照模3同余合并即可。
    bzoj2870(没做)
    震波
    动态点分的一道练手题。
    这道题普通点分可以这样做(没有修改):
    设当前询问点为x
    求出当前重心后,枚举每棵子树。如果当前点连通块没有询问点时返回。
    枚举每个连通块,递归计算距离<=k的点的点权和。
    实际上我们接下来要求的是除了当前连通块,距离x的点<=k的点的点权和。
    设d为子树重心->现在重心的距离。则实际上我们要求现在重心c的除了递归到的子树距离<=k-d的和。
    这个可以容斥。把当前点距离<=k-d的值减去递归点距离<=k-d的值即可。
    每次不会递归到不包含x的连通块,连通块的大小至少会/2,所以根据等比数列求和,时间复杂度O(n)。
    这道题要求距离一个点<=k的点的点权和,所以可以使用线段树维护一下一个点子树的点权。
    如果有修改的话,在点分树的每个点维护2棵线段树,线段树的每个下标存储距离=d的点。
    每次修改时跳点分树并且修改距离,查询时维护上面提到的信息即可。
    [ZJOI2015]幻想乡战略游戏(没做)
    树上游戏
    这道题有O(n)做法,但是为了练习点分治,所以使用点分做这道题。
    我们设当前的连通块的重心为c,则我们要统计过c的路径对每一个当前连通块x的贡献。
    假设x在重心的子树y里。
    设sum表示除x子树内对当前点的贡献,cv[x]表示除了x的y子树中对x的贡献
    贡献分为2部分:
    1.另一个点z->c的子树上的颜色对x的贡献。
    2.c->x对x的贡献。
    第一部分可以维护一个桶c[x]表示x颜色的出现次数。
    使用一个dfs统计。在更新c时可以顺便算出cv。
    第二个dfs统计sum。
    十分抽象,建议看代码。
    #include<bits/stdc++.h>
    using namespace std;
    #define int long long
    #define N 200010
    int h[N],vi[N],ms[N],rt,v[N],n,nxt[N],ec,c[N],sz[N],ans[N],cc[N],ss,cv[N];
    void add(int x,int y){v[++ec]=y;nxt[ec]=h[x];h[x]=ec;}
    void gr(int x,int fa){
        ms[x]=0;
        sz[x]=1;
        for(int i=h[x];i;i=nxt[i])
            if(v[i]!=fa&&!vi[v[i]]){
                gr(v[i],x);
                sz[x]+=sz[v[i]];
                ms[x]=max(ms[x],sz[v[i]]);
            }
        ms[x]=max(ms[x],n-sz[x]);
        if(ms[x]<ms[rt])rt=x;
    }
    void d1(int x,int fa,int op){
        if(!cc[c[x]]){
            ss+=sz[x]*op;
            cv[c[x]]+=sz[x]*op;
        }
        cc[c[x]]++;
        for(int i=h[x];i;i=nxt[i])
            if(v[i]!=fa&&!vi[v[i]])
                d1(v[i],x,op);
        cc[c[x]]--;
    }
    void d2(int x,int fa){
        if(!cc[c[x]])ss+=n-cv[c[x]];
        cc[c[x]]++;
        ans[x]+=ss;
        for(int i=h[x];i;i=nxt[i])
            if(v[i]!=fa&&!vi[v[i]])
                d2(v[i],x);
        cc[c[x]]--;
        if(!cc[c[x]])ss-=n-cv[c[x]];
    }
    void dfs(int x){
        vi[x]=1;
        d1(x,0,1);
        ans[x]+=ss;
        for(int i=h[x];i;i=nxt[i])
            if(!vi[v[i]]){
                cv[c[x]]-=sz[v[i]];
                n-=sz[v[i]];
                ss-=sz[v[i]];
                cc[c[x]]=1;
                d1(v[i],x,-1);
                cc[c[x]]=0;
                d2(v[i],x);
                cc[c[x]]=1;
                d1(v[i],x,1);
                ss+=sz[v[i]];
                cc[c[x]]=0;
                cv[c[x]]+=sz[v[i]];
                n+=sz[v[i]];
            }
        d1(x,0,-1);
        for(int i=h[x];i;i=nxt[i])
            if(!vi[v[i]]){
                n=sz[v[i]];
                rt=0;
                gr(v[i],x);
                gr(rt,0);
                dfs(rt);
            } 
    }
    signed main(){
        cin>>n;
        int tn=n;
        for(int i=1;i<=n;i++)
            cin>>c[i];
        for(int i=1;i<n;i++){
            int x,y;
            cin>>x>>y;
            add(x,y);
            add(y,x);
        }
        ms[0]=1e9;
        gr(1,0);
        gr(rt,0);
        dfs(rt);
        for(int i=1;i<=tn;i++)
            cout<<ans[i]<<'
    ';
    }
    View Code
    [CTSC2010]珠宝商
    这道题实际上不是个点分治题。
    暴力1:直接枚举一个点,然后使用sam上跑统计答案。
    暴力2:建出原串的后缀树,可以用sam建出反串后构建。
    考虑一个更"高级"的暴力,每次将贡献在lca处统计。
    这个暴力会遍历lca的子树,统计一个点在原字符串上作为前半部分和后半部分的贡献。实际上就是要求以原串每个点开头/结尾的串。
    可以枚举每个子树上的点然后打标记,最后在sam上下推标记(遍历整个sam)
    暴力2显然可以用点分优化。但是每次下推标记的时间复杂度还是瓶颈。
    实际上,当子树大小较小时可以使用sam在原串上跑来统计答案,子树大小较大时可以使用暴力2,这样子被推标记的次数就不会很多了。
    时间复杂度降到nsqrt(n)
    [ZJOI2007]捉迷藏(没用点分做)
    [HNOI2015]开店(不熟)
    [SCOI2016]幸运数字
    这道题是树上路径问题,显然可以考虑点分。
    实际上,原题可以任意选出点,得到这个点的权值,答案是所有权值的异或,所以可以考虑使用线性基。
    题目就是让我们维护一条链的线性基。
    这个用树剖/倍增的时间复杂度是3个log的,有点卡常。
    但是可以使用点分。使用2个dfs。第一个dfs构建点分树。接下来每个询问的2个值在点分树上跳lca,把询问挂在lca上。
    第二个dfs统计答案。维护当前分治重心->每个点的线性基,每次询问的时候把2个对应的线性基合并即可。
    合并的方法就是把一个线性基的元素插入到另一个里面。
    这样子的时间复杂度比倍增法更优越,但是跑的更慢,我不知道是为什么。
    [WC2018]通道
    这道题可以使用随机化爬山通过,但是使用这种方法通过此题意义不大。
    直径有一个重要的性质:如果边权是非负的,把2个点集合并起来,新的直径肯定是在这2个点集直径的4个端点内产生。
    原因是(这个网上大多没讲):在一个点集中,距离一个点的最远点一定是直径的端点(如果不是,则可以挪移直径的端点)
    实际上,在新树中,如果直径的端点不在这4个点产生,则距离某个和它在同一个原点集的点的最远点不是直径的端点,则与上述假设矛盾。
    现在设$d_k(i,j)$表示第k颗树上的距离,$dep_k(i)$表示第k颗树上点i的深度
    对第一棵树进行边分治,假设2边的连通块被染色成黑,白,现在的边是(a,b),题目转化成要找到一个点对(c,d),让$d_2(c,d)+d_3(c,d)+d_1(a,c)+d_1(c,b)$最大。
    建立当前边分块左边,右边在t2上的虚树。考虑t2上每一个点l作为lca对答案的贡献。则问题转化成让$dep_2(c)+dep_2(d)-dep_2(l)+d_3(c,d)+d_1(a,c)+d_1(c,b)$最大
    实际上就是让$(dep_2(c)+d_1(a,c))+(dep_2(d)+d_1(c,b))+d_3(c,d)-dep_2(l)$最大,后面的$dep_2(l)$是常数。
    前面的2项可以在t3上新建点t向c/d连边$(dep_2(c)+d_1(a,c))(dep_2(d)+d_1(c,b))$,设f(i,0)表示i点为白色的最优解,f(i,1)表示i点为黑色的最优解。
    每次用每个节点组合更新答案。
    时间复杂度O(nlog^2n),可以使用离线+基数排序+rmqlca优化到nlogn
    河童重工(没做)
    这道题的mst显然不能暴力建。
    [CTSC2018]暴力写挂
    [清华集训2016]汽水
    路径相关问题要使用点分。
    先二分一下绝对值的最小值,设为x,则限制条件是:$|frac{s}{m}-k|<=x$,s是路径权值和,m是边数。
    再把绝对值拆掉,变成$-x<=frac{s}{m}-k<=x$
    同乘m,得到$-xm<=s-km<=xm$
    考虑$-xm<=s-km$,移项可得到$0<=sum a[i]-km+xm$和$0<=sum (a[i]-k+x)$
    右边可得$s-km-xm<=0$和$sum (a[i]-k-x)<=0$
    如果令v[i]=a[i]-k,可以得到$sum (v[i]-x)<=0$和$0<=sum (v[i]+x)$
    点分一下。如果要符合条件,就要存在一条路径满足$sum (v[i]-x)<=0$和$0<=sum (v[i]+x)$
    考虑计算$sum (v[i]-x)<=0$的条件,
    实际上,不用整体套一个二分,可以在点分的时候二分。每次把左端点设1,右端点设为ans-1
    如果我们在判定v[i]-x的话,可以把权值从小到大排序,使用双指针,这样子另外一边符合要求的是一个区间[r,n],设左边的指针是l,则随着l的增长,r不会增加。注意在寻找最小值时,最小值不能来自现在这颗子树,要记录不同于当前子树的最小值进行转移。
    [xr2]永恒
    [WC2014]紫荆花之恋
    这道题有路径,显然可以考虑点分。
    实际上,当没有修改时,假设现在分治中心为c,设d[i]表示i->c的距离,则问题转化为要求点对d[i]+d[j]<=r[i]+r[j]的点的个数。移项得到d[i]-r[i]<=r[j]-d[j]。可以插入每个d[i]-r[i],查询<=r[j]-d[j]的个数,使用平衡树解决。
    有修改的情况树就是不是固定的。在每个节点上维护一个平衡树。可以直接把修改的2个节点在点分树上连边,容易发现这样子新树(原点分树插入新点的结果)还是一个点分树。
    加入边后,在点分树上插入点的所有父亲的平衡树都要插入权值。
    这样子时间复杂度是错的,因为可能插入的是一条链,这样子每次会在所有节点的平衡树上都插入一个数,时间复杂度会退化。
    可以仿照替罪羊树的思想,当一颗子树占整颗点分树的大小太大,就重新分治,重构点分树和它的平衡树。
    可以仿照替罪羊树证明复杂度是nlog^2n
    这道题的思想十分经典,值得学习。
  • 相关阅读:
    重写保存按钮save事件
    隐藏列获取不到值,表格选中行提示未选中
    前后台获取上下文context
    editGrid分录表格
    通用查询-高级查询
    js保留位和取整
    在Visual Studio中使用C++创建和使用DLL
    Lua中的一些库(1)
    Lua中的面向对象编程
    Lua中的模块与包
  • 原文地址:https://www.cnblogs.com/cszmc2004/p/12765736.html
Copyright © 2020-2023  润新知