zoukankan      html  css  js  c++  java
  • 长链剖分学习笔记

    长链剖分和重链剖分类差不多,都是解决树上问题的一些trick。

    下面定义(dep[u])表示(u)到根节点路径上有多少条边((u)的深度),(mdep[u])表示(u)子树内的所有点的深度的最大值。

    对比重链剖分选择(size)最大的儿子作为重儿子,长链剖分选择(mdep)最大的儿子作为“重”儿子(这样方便优化一些树上以深度为下标的DP)。

    还是直接来看题吧QAQ

    LuoguP5903 【模板】树上 k 级祖先

    https://www.luogu.com.cn/problem/P5903

    这题搞法挺多的...而且实际跑下来长链剖分也不是很快...但还是讲一下吧

    不难想到一些单次询问在(O(log n))时间内回答的做法,如果重链剖分后二分重链的话可以做到(O(log log n)),但这些都不是重点,长链剖分可以做到(O(n log n))预处理,单次(O(1))回答 别问我为什么这样还跑的那么慢...

    首先长链有一个性质,对于点(u),它的(k)级祖先所在的长链的长度一定大于或等于(k)

    这很显然,因为如果那条长链的长度小于(k)的话直接用(u)(k)级祖先到(u)这一段就可以得到一条更长的链,与长链的定义矛盾。

    然后有啥子用呢?我们先预处理出(1...n)(n)个数二进制下的最高位(high-bit),然后第一遍(Dfs)时算一个数组(p[x][i])表示(x)向上跳(2^i)步所到达的节点。

    然后对于一个询问((u,k)),令(h=2^{lfloorlog_2k floor}),先把(u)调到(f[u][h])上,此时(u)还要向上跳(k-h)层,此时(u)所在重链的长度一定(ge h>k-h),所以直接将(u)跳至链顶,判断向上或者向下跳即可。

    也就是说对于一跳重链的链顶(u),维护两个数组(up[u][0...len],down[u][0...len])(len)表示长链长度)。

    void dfs1(int u,int f,int deep) {
        dep[u]=deep,mdep[u]=dep[u];
        p[u][0]=f; 
        rep(i,1,LOGN) p[u][i]=p[p[u][i-1]][i-1];
        for (auto v:e[u]) if (v!=f) {
            dfs1(v,u,deep+1);
            if (mdep[v]>mdep[u]) 
                mdep[u]=mdep[v],son[u]=v;
        }
    }
    
    void dfs2(int u,int topf) {
        top[u]=topf;
        if (u==top[u]) {
            for (int x=u,i=0;i<=mdep[u]-dep[u];i++,x=son[x]) dn[u].pb(x);
            for (int x=u,i=0;i<=mdep[u]-dep[u];i++,x=p[x][0]) up[u].pb(x);
        }
        if (!son[u]) return;
        dfs2(son[u],topf);
        for (auto v:e[u]) if (v!=p[u][0]&&v!=son[u]) dfs2(v,v);
    }
    
    // lg[n]=floor(log2(n))
    
    int query(int u,int k) {
        if (k==0) return u;
        u=p[u][lg[k]],k-=(1<<lg[k])+dep[u]-dep[top[u]],u=top[u];
        return k>=0?up[u][k]:dn[u][-k];
    }
    

    (但实际上这个题并没有什么用,只是熟悉一下长链剖分QAQ)

    CF1009F Dominant Indices

    Luogu的链接

    定义(d(u,k))表示(u)子树中到(u)的距离为(k)的点的个数,对于每个(u),你需要找到最小的(k),使得(d(u,k))最大。

    考虑把(d)全算出来,不难发现有如下DP

    [d(u,k)=sumlimits_{v in son(u)} d(v,k-1) ]

    特别的(d(u,0)=1)

    这是一个(n^2)的做法,考虑用长链剖分优化。

    (len[u])表示(u)到叶子节点的最大距离。

    那么只用考虑(d(u,0)...d(u,len[u]))

    对于点(u),我们直接让他的重儿子(v),与(d(u,1...len[u]))共用同一空间,然后对于(u)向下链的一些重链,的儿子为链首的一些重链,直接将信息合并到(d(u,0...len[u]))上即可。

    这样对每条重链只会合并一次,单次合并的复杂度是(O(1))的,所以最后复杂度为(O(n)),也只需要开(O(n))的空间(下面代码内存池开的naive了...)

    可能有点抽象...看代码吧QAQ

    #include <bits/stdc++.h>
    using namespace std;
    #define rep(i,a,n) for (int i=a;i<n;++i)
    #define per(i,a,n) for (int i=n-1;i>=a;--i)
    #define mp make_pair
    #define pb push_back
    #define fi first
    #define se second
    #define all(x) (x).begin(),(x).end()
    #define SZ(x) ((int)(x).size())
    typedef double db;
    typedef long long ll;
    typedef pair<int,int> PII;
    typedef vector<int> VI;
     
    const int N=1e6+10;
    namespace Pool {
        int buf[N*23],*p=buf;
        inline int* New(int len) {int *ret=p; p+=len; return ret;}
    }
    using Pool::New;
     
    int n,fa[N],dep[N],mdep[N],son[N];
    VI e[N];
     
    void dfs(int u,int f,int deep) {
        dep[u]=mdep[u]=deep;
        fa[u]=f;
        for (auto v:e[u]) if (v!=f) {
            dfs(v,u,deep+1);
            if (mdep[v]>mdep[u])
                mdep[u]=mdep[v],son[u]=v;
        }
    }
     
    int *f[N],ans[N];
     
    void dp(int u) {
        if (son[u]) {
            int v=son[u];
            f[v]=f[u]+1,dp(v);
            ans[u]=ans[v]+1;
        }
        f[u][0]=1;
        if (f[u][0]>=f[u][ans[u]]) ans[u]=0;
        int mx=f[u][ans[u]];
        for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
            f[v]=New(mdep[v]-dep[v]+1);
            dp(v);
            for (int i=1;i<=mdep[v]-dep[v]+1;i++) {
                f[u][i]+=f[v][i-1];
                if (f[u][i]>mx||(f[u][i]==mx&&i<ans[u])) ans[u]=i,mx=f[u][i];
            }
        }
    }
     
    int main() {
    #ifdef LOCAL
        freopen("a.in","r",stdin);
    #endif
        scanf("%d",&n);
        rep(i,0,n-1) {
            int u,v; scanf("%d%d",&u,&v);
            e[u].pb(v),e[v].pb(u);
        }
        dfs(1,0,1);
        f[1]=New(mdep[1]-dep[1]+1);
        dp(1);
        rep(i,1,n+1) printf("%d
    ",ans[i]);
        return 0;
    }
    

    LuoguP3899 [湖南集训]谈笑风生

    https://www.luogu.com.cn/problem/P3899

    这题有多种数据结构做法......但这里讲的是长链剖分...

    首先一种情况是(b)(a)的祖先,这个很显然,我们不考虑。

    对于(b)(a)的子树内,我们考虑DP

    (f[u][i])表示(u)子树内到(u)距离为(i)的所有(v)(siz[v]-1)的和((siz[u])表示(u)的子树大小)。

    有转移(f[u][i]=sumlimits_{v in son(u)} f[v][i-1])

    那么这一部分的贡献就是(sumlimits_{i le k} f[a][i])

    这样是一个n^2的DP。

    考虑优化,发现这里优化不太好做,将状态的定义改成前缀和又不太好转移,那没事,改后缀和就好了。

    转移的柿子不变,只是对于(f[u][0])要将他加上所有(f[v][0])的和以及(siz[u]-1)

    长链剖分优化这个DP即可,还有注意这是一个离线的做法(因为内存会被覆盖),所以要算完就回答和(u)有关的问题。

    复杂度(O(n+q))

    #include <bits/stdc++.h>
    using namespace std;
    #define rep(i,a,n) for (int i=a;i<n;++i)
    #define per(i,a,n) for (int i=n-1;i>=a;--i)
    #define mp make_pair
    #define pb push_back
    #define fi first
    #define se second
    #define all(x) (x).begin(),(x).end()
    #define SZ(x) ((int)(x).size())
    typedef double db;
    typedef long long ll;
    typedef pair<int,int> PII;
    typedef vector<int> VI;
    
    const int N=3e5+10;
    struct Pool {
        ll buf[N<<3],*pt;
        Pool() {pt=buf;}
        inline ll *New(int sz) {ll *ret=pt; pt+=sz; return ret;}
    }p;
    
    VI e[N];
    int n,Q,mdep[N],dep[N],son[N];
    int fa[N],len[N],siz[N];
    ll *f[N];
    
    void dfs(int u,int ff) {
        fa[u]=ff,siz[u]=1;
        mdep[u]=dep[u]=dep[ff]+1;
        for (auto v:e[u]) if (v!=ff) {
            dfs(v,u),siz[u]+=siz[v];
            if (mdep[v]>mdep[u]) {
                mdep[u]=mdep[v];
                son[u]=v;
            }
        }
        len[u]=mdep[u]-dep[u]+1;
    }
    
    vector<PII> qs[N];
    ll ans[N];
    
    void dp(int u) {
        if (son[u]) {
            f[son[u]]=f[u]+1;
            dp(son[u]);
            f[u][0]+=f[son[u]][0];
        }
        f[u][0]+=siz[u]-1;
        for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
            f[v]=p.New(len[v]<<1);
            dp(v);
            f[u][0]+=f[v][0];
            rep(i,0,len[v]) f[u][i+1]+=f[v][i];
        }
        for (auto q:qs[u]) {
            int k=q.fi,id=q.se;
            ans[id]=1ll*(siz[u]-1)*min(k,dep[u]-1);
            if (k+1<len[u]) ans[id]+=f[u][0]-f[u][k+1]-siz[u]+1;
            else ans[id]+=f[u][0]-siz[u]+1;
        }
    }
    
    int main() {
    #ifdef LOCAL
        freopen("a.in","r",stdin);
    #endif
        scanf("%d%d",&n,&Q);
        rep(i,0,n-1) {
            int u,v; scanf("%d%d",&u,&v);
            e[u].pb(v),e[v].pb(u);
        }
        dfs(1,0);
        f[1]=p.New(len[1]<<1);
        rep(i,1,Q+1) {
            int u,k; scanf("%d%d",&u,&k);
            qs[u].pb(mp(k,i));
        }
        dp(1);
        rep(i,1,Q+1) printf("%lld
    ",ans[i]);
        return 0;
    }
    

    P5904 [POI2014]HOT-Hotels

    https://www.luogu.com.cn/problem/P3565

    https://www.luogu.com.cn/problem/P5904(加强版 需要长链剖分优化)

    这题更侧重思维...并不是后来的优化......

    对于三元组((a,b,c)),我们考虑枚举(LCA(a,b,c)),换句话说在(LCA)处算它的贡献。

    也就是说Dfs的时候对于点u,枚举儿子v,然后考虑一或两个点在v子树内,其他点在u别的儿子内时的合法三元组个数(或者别的点就是u)。

    首先不难想到维护一个东西(f[u][i])表示(u)子树内到(u)的距离为(i)的点的个数。

    然后再维护一个(g[u][i])表示(u)子树内有多少个二元组((v,w)),满足(d(v,lca_{v,w})=d(w,lca_{v,w})=d(lca_{v,w},u)+i)(d(i,j))表示树上点(i)到点(j)的距离,(lca)就字面意思啦)。

    那么再枚举u的儿子v时,先不要更新u的f和g,枚举(i),用(f[v][i]*g[u][i+1]+g[v][i]*f[u][i-1]),更新答案(建议画图理解QAQ)。

    然后考虑如何更新(f)(g)。还是枚举(i),用(f[v][i])更新(f[u][i+1])(g[v][i])更新(g[u][i-1]),对于(g),还有一种情况是一个点在(v)的子树内,另一个点在前面遍历的子树内或另一点是(u),对于这种情况用(f[v][i]*f[u][i+1])更新(g[u][i+1])

    用长链剖分优化上述DP即可,注意对(g)要开两倍的空间(因为要吧g[u]-1给g[v])。

    #include <bits/stdc++.h>
    using namespace std;
    #define rep(i,a,n) for (int i=a;i<n;++i)
    #define per(i,a,n) for (int i=n-1;i>=a;--i)
    #define mp make_pair
    #define pb push_back
    #define fi first
    #define se second
    #define all(x) (x).begin(),(x).end()
    #define SZ(x) ((int)(x).size())
    typedef double db;
    typedef long long ll;
    typedef pair<int,int> PII;
    typedef vector<int> VI;
    
    const int N=1e5+10;
    
    namespace Pool {
        ll buf[N<<3],*p=buf;
        inline ll *New(int len) {ll *ret=p; p+=len; return ret;}
    }
    using Pool::New;
    
    VI e[N];
    int n,len[N],fa[N],son[N];
    ll *f[N],*g[N],ans,fs[N],gs[N];
    
    void dfs(int u,int ff) {
        fa[u]=ff;
        int mx=0;
        for (auto v:e[u]) if (v!=ff) {
            dfs(v,u);
            if (len[v]>mx) mx=len[v],son[u]=v;
        }
        len[u]=mx+1;
    }
    
    void dp(int u) {
        if (son[u]) {
            int v=son[u];
            f[v]=f[u]+1,g[v]=g[u]-1;
            dp(v);
        }
        ans+=g[u][0],f[u][0]=1;
        for (auto v:e[u]) if (v!=fa[u]&&v!=son[u]) {
            f[v]=New(len[v]<<1),g[v]=New(len[v]<<1);
            dp(v);
            rep(i,0,len[v]) {
                if (i>0) ans+=g[v][i]*f[u][i-1];
                ans+=f[v][i]*g[u][i+1];
            }
            rep(i,0,len[v]) {
                g[u][i+1]+=f[v][i]*f[u][i+1];
                if (i>0) g[u][i-1]+=g[v][i];
            }
            rep(i,0,len[v]) f[u][i+1]+=f[v][i];
        }
    }
    
    int main() {
    #ifdef LOCAL
        freopen("a.in","r",stdin);
    #endif
        scanf("%d",&n);
        rep(i,0,n-1) {
            int u,v; scanf("%d%d",&u,&v);
            e[u].pb(v),e[v].pb(u);
        }
        dfs(1,0);
        f[1]=New(len[1]<<1),g[1]=New(len[1]<<1);
        dp(1);
        printf("%lld
    ",ans);
        return 0;
    }
    

    LuoguP4292 [WC2010]重建计划

    蒟蒻的题解


    总的来讲一些逼格很高的题目注重的并不是什么奇怪的优化...珂能还是考察思维能力QAQ

  • 相关阅读:
    在线自动创建springboot工程
    java线程自带队列的使用以及线程阻塞
    如何分析java内存泄漏问题
    java接口入参模板化,适用于企业化服务远程调度模板化的场景,接口入参实现高度可配置化
    打造springboot高性能服务器(spring reactor的使用)
    docker 5 docker-阿里云加速配置
    docker 4 docker的三要素
    docker 3 docker安装
    docker 2 docker介绍
    docker 1 为什么要使用docker
  • 原文地址:https://www.cnblogs.com/wxq1229/p/12540188.html
Copyright © 2011-2022 走看看