• 与图论的邂逅08:树上倍增


    什么是树上倍增?

    顾名思义,就是在树上成倍地增长,可以用于解决一些静态树的查询问题。


    放出例题:给定一棵根节点为1的n个节点的树,并给出树上两个点u,v,求它们的最近公共祖先。

    我们可以预处理出每个点的父亲节点fa和深度dep,然后选择u,v中深度较大者不断地往父亲节点走,当u,v深度相同时判断:u,v是否为同一个点。如果是,那么答案就是u或者v;否则,u,v同时向着各自的父亲节点走,当两个点的父亲节点相同时,fa[u]或者fa[v]就是答案。时间复杂度为O(N)。

    //口胡一段代码(未检查)
    int fa[maxn],dep[maxn];
    //预处理部分,时间复杂度为O(N)
    void dfs_getfa(int u,int pre){
        for(register int i=head[u];~i;i=e[i].next){
            int v=e[i].to;
            if(v==fa[u]) continue;
            dep[v]=dep[u]+1,fa[v]=u;
            dfs_getfa(v,u);
        }
    }
    //求lca部分,时间复杂度为O(N)
    inline int lca(int u,int v){
        if(dep[u]>dep[v]) swap(u,v);
        while(dep[fa[v]]>=dep[u]) v=fa[v];
        if(u==v) return u;
        while(fa[u]!=fa[v]) u=fa[u],v=fa[v];
        return fa[u];
    }
    

    现在问题改一下,仍然给你一棵根节点为1的n个点的树,下面有m个询问,每个询问都给出两个点u,v,求每个询问的lca(u,v)。1≤n,m≤500000。

    如果还是之前的做法,那么时间复杂度就是O(NM),肯定会爆掉对吧,,,,,,所以这里我们将会用到一种算法——树上倍增。之前只是一步一步地跳实在太慢,我们一次跳个2的k次方步如何?根据二进制转化的思想,一个数x总能背拆成如下的样子:

    [x=2^{k_1}+2^{k_2}+2^{k_3}+...... ]

    所以每次跳2的若干次方,我们总能跳到想要的位置。还是用上面的思路,先预处理出深度dep和数组fa(i,j),表示节点i往上跳2的j次方步到达的节点,那么f(i,j)=f(f(i,j-1),j-1),初始化f(i,j)为i的父亲节点。然后选择深度大的节点先跳,再两个点一起跳。时间复杂度为O((N+M)logN)。

    int fa[maxn][20],dep[maxn],maxdep;
    //预处理部分,时间复杂度为O(NlogN)
    void dfs_getfa(int u,int pre){
        for(register int i=head[u];~i;i=e[i].next){
            int v=e[i].to;
            if(v==pre) continue;
            dep[v]=dep[u]+1,fa[v][0]=u;
            for(register int j=1;j<=maxdep;j++) fa[v][j]=fa[fa[v][j-1]][j-1];
            dfs_getfa(v,u);
        }
    }
    //求lca部分,单次时间复杂度为O(logN)
    inline int lca(int u,int v){
        if(dep[u]>dep[v]) swap(u,v);
        for(register int i=maxdep;i>=0;i--) if(dep[fa[v][i]]>=dep[u]) v=fa[v][i];
        if(u==v) return u;
        for(register int i=maxdep;i>=0;i--) if(fa[u][i]!=fa[v][i]) u=fa[u][i],v=fa[v][i];
        return fa[u][0];
    }
    //main函数中
    	maxdep=(int)log(n)/log(2)+1;
    

    上面是树上倍增最简单的实例。下面再给出一道例题:

    给出一棵n个节点的树,下面有m次询问,每次询问给出树上两个点u,v,求u到v的路径上边权的最大值。

    明确思路。虽然路径上有很多个点,但我们可以用唯一的三个点来确定这条路径——u,v和lca(u,v)。所以问题可以转化为:求出u到lca(u,v)的路径上边权的最大值为max1,并求出v到lca(u,v)的路径上边权的最大值max2,答案为max(max1,max2)。而我们上面单可以跳到lca罢了。可以类比着求fa(i,j)的做法,设max_edge(i,j)表示节点i到f(i,j)的路径上边权最大值,那么显然:max_edge(i,j)=max(max_edge(i,j-1),max_edge(fa(i,j-1),j-1)),初始化max_edge(i,0)为i和i的父亲节点之间的边的边权。然后我们倍增地往lca跳,统计跳过的地方的边权最大值即可,时间复杂度也是O((N+M)logN)。

    int fa[maxn][20],max_edge[maxn][20],dep[maxn],maxdep;
    void dfs_getfa(int u,int pre){
        for(register int i=head[u];~i;i=e[i].next){
            int v=e[i].to;
            if(v==pre) continue;
            dep[v]=dep[u]+1,fa[v][0]=u,max_edge[v][0]=e[i].dis;
            for(register int j=1;j<=maxdep;j++) fa[v][j]=fa[fa[v][j-1]][j-1],max_edge[v][j]=max(max_edge[v][j-1],max_edge[fa[v][j-1]][j-1]);
            dfs_getfa(v,u);
        }
    }
    inline int lca(int u,int v){
        if(dep[u]>dep[v]) swap(u,v);
        int ans=-INF;
        for(register int i=maxdep;i>=0;i--) if(dep[fa[v][i]]>=dep[u]) ans=max(ans,max_edge[v][i]),v=fa[v][i];
        if(u==v) return ans;
        for(register int i=maxdep;i>=0;i--) if(fa[u][i]!=fa[v][i]) ans=max(ans,max(max_edge[u][i],max_edge[v][i])),u=fa[u][i],v=fa[v][i];
        return max(ans,max(max_edge[u][0],max_edge[v][0]));
    }
    //main函数中
    	maxdep=(int)log(n)/log(2)+1;
    

    求最小值或者边权和也是类似的做法。



    再来一道例题:给出一棵n个节点的树,下面有m次操作,"1,i,w"表示修改编号为i的边的边权为w,"2,u,v"表示求节点u,v之间的路径上边权最大值。

    这道题倍增就做不了了,因为无法快速修改max_edge数组。这得让树剖来做,时间复杂度可以做到O(MlogNlogN)。所以,树上倍增只适合做静态树(不带修改的)上的问题。当然树剖也可以做静态树的问题,但树上倍增表现更好(除了求lca).

  • 相关阅读:
    【原创】QTP中手动添加对象
    【转载】【缺陷预防技术】流程技术预防
    【资料】HP Loadrunner 11下载地址
    使用命令行操作VSS
    sql server 按时间段查询记录的注意事项
    Asp.net应用程序文件名重名引起的bug
    使用SQL语句查询表中重复记录并删除
    backgroundpositionx的兼容性问题
    关于Asp.net Development Server
    如何查看正在使用某个端口的应该程序
  • 原文地址:https://www.cnblogs.com/akura/p/10890805.html
Copyright © 2020-2023  润新知