树上倍增学习总结

提到树上倍增就不得不先说说最近公共祖先(LCA)了
如下图所示
4和5的LCA即为2(绿色的)
LCA图
那怎么求LCA呢?
最简单粗暴的方法就是先深搜一次,处理出每个节点的深度
DFS深度
然后把深度较深的那一个点一层层地往上跳,直到到达某一点(和另一个深度一样)
然后两个点一起一层层地往上跳,知道到达某个点(就是LCA)时两个点重合。

以点 4 和点 5 为例,点 4 的深度较深,所以点 4 一层层往上跳,直到到达和点 5 深度一样的点 3,然后点 3 和点 5 一起一层层往上跳直到到达点 2(LCA),两点重合。

过程
不过,大家应该发现一层层地跳时间复杂度会很高
如果一下子跳到目标点,内存又不支持
所以大🐂们找到了一种新的算法—树上倍增
时间复杂度为 O(n * log n)
倍增思想是二进制
  首先开一个 n×longn 的数组,比如 fa[n][longn],其中 fa[i][j] 表示 i 节点的第 2j 个父亲是谁
然后,我们会发现有这么一个性质:fa[i][j] = fa[fa[i][j-1][j-1]
也就是,i 的第 2j 个父亲是 i 的第 2j-1 个父亲的第 2j-1 个父亲
这样 ,本来我们求 i 的第 k 个父亲的复杂度是 O(k),现在复杂度变成了O(longk)。
  我们知道,一个数的二进制形式中,如果右边的数第 i 位上是 1,表示这个数如果分解位若干个 2 的次幂的和的形式,其中有一项一定是 2i-1。举个例子:10的二进制表示为1010,它的第 2 位和第 4 位上是 1,所以 10 = 21 + 23
  下面是求 i 的第 k 个父亲的代码段:

int father(int i,int k)
{
	int mk = (int)(log(k*1.0)/log(2.0))+1;
    for(int x=0;x<=mk;x++){
        if((1<<x)&k){       //(1<<x)&k可以判断k的二进制表示中,第(x-1)位上是否为1
            i=fa[i][x];    //把i往上提
        }
    }
    return i;
}

  我们可以通过一次 dfs 处理出 fa数组:(dep[i] 表示 i 的深度,这个可以一起处理出来,以后要用)
如果待处理的树有 n 个节点,那么最多有一个节点会有 2logn 个父亲,所以我们的 fa 数组第二维开 logn就够了。
这里用 max0表示 logn。初始化 fa 为 0 ,若 fa[i][j] = 0 表示 i 没有第 2j 个父亲。

void dfs(int x)
{
    for(int i=1;i<=max0;i++){
        if(fa[x][i-1])   //在dfs(x)之前,x的父辈们的fa数组都已经计算完毕,所以可以用来计算x
            fa[x][i]=fa[fa[x][i-1]][i-1];
        else break;    //如果x已经没有第2^(i-1)个父亲了,那么也不会有更远的父亲,直接break
    }
    for(/*每一个与x相连的节点i*/)
        if(i!=fa[x][0]){    //如果i不是x的父亲就是x的儿子
            fa[i][0]=x;       //记录儿子的第一个父亲是x
            dep[i]=dep[x]+1;      //处理深度
            dfs(i);
        }
}
memset(fa,0,sizeof(fa));

  这样,我们在nlogn 的内可以通过一遍 dfs 处理出这棵树的相关信息。然后就可以在 nlogn 的时间内完成一些操作。
  倍增的应用中,最基础的应该是求 LCA (最近公共祖先),时间复杂度是 logn。
  对于求 u、v 的 LCA,我们可以先把 u、v 用倍增法把深度大的提到和另一个深度相同。如果此时 u、v 已经相等了,表示原来 u、v 就在一条树链上,知道返回此时的结果。
  如果此时u、v深度相同但不相同,则证明他们的 LCA 在更“浅”的地方,此时需要把u、v一起用倍增法上提到他们的父亲相等。为啥是提到父亲相等呢?因为倍增法是一次上提很多,所以有可能提“过”了,如果是判断他们本身作为循环终止条件,就无法判断是否提得过多了,所以要判断他们父亲是否相等。不懂的可以详见代码:

int max0 = (int)(log(n*1.0)/log(2.0))+1;	//或者max0 = 20
int LCA(int u,int v)
{
    if(dep[u]<dep[v])   swap(u,v);  //我们默认u的深度一开始大于v,那么如果u的深度小就交换u和v
    int delta = dep[u] - dep[v];    //计算深度差
    for(int x=0;x<=max0;x++){    //此循环用于提到深度相同。
        if((1<<x)&delta)
            u=fa[u][x];
    }
    if(u==v)    return u;
    for(int x=max0;x>=0;x--){     //注意!此处循环必须是从大到小!因为我们应该越提越“精确”,
        if(fa[u][x]!=fa[v][x]){   //如果从小到大的话就有可能无法提到正确位置,自己可以多想一下
            u=fa[u][x];
            v=fa[v][x];
        }
    }
    return fa[u][0];    //此时u、v的第一个父亲就是LCA。
}
 

  倍增还可以有很多变化,这让倍增法可以有更多的变化。比如用data[i][j]记录 i 到他的第 2j 个父亲的路径长度,就可以求出 LCA 边求出两点距离,因此 data[i][j] 满足倍增的递推式:data[i][j]=data[i][j-1]+data[fa[i][j-1]][j-1]。
  求解两点x,y之间的距离,就是 x 到 LCA,LCA 到 y 两段距离之和。

	//dfs在 fa 数组求解和求深度之间加这一句
    for(int i=1;i<=20;i++)
        da[x][i] = da[x][i-1] + da[fa[x][i-1]][i-1];
    memset(da,0,sizeof(da));
int dis(int x,int y){
    int rtn = 0;
    int l = LCA(x,y);
    for(int i=20;i>=0;i--){		// x 到 LCA 的距离
        if(dep[fa[x][i]]>=dep[l]){
            rtn += da[x][i];
            x = fa[x][i];
        }
    }
    for(int i=20;i>=0;i--){		// LCA 到 y 的距离
        if(dep[fa[y][i]]>=dep[l]){
            rtn += da[y][i];
            y = fa[y][i];
        }
    }
    return rtn;
}

  求解 x 到 y 的第 k 个节点是什么

int kth(int x,int y,int k){
    int l = LCA(x,y);
    if(k<=dep[x]-dep[l]){	//第 k 个节点在 x 到 LCA 链之间
        k -= 1;
        for(int i=20;i>=0;i--){
            if(1<<i<=k){
                x = fa[x][i];
                k -= 1<<i;
            }
        }
        return x;
    }
    else if(k>dep[x]-dep[l]){	//第 k 个节点在 LCA 到 y 链之间
        k = (dep[y]-dep[l]-(k-(dep[x]-dep[l])))+1;
        for(int i=20;i>=0;i--){
            if(1<<i<=k){
                y = fa[y][i];
                k -= 1<<i;
            }
        }
        return y;
    }
}

  或者用 maxlen[i][j] 记录 i 到第 2j 个父亲的路径上最长边的边权,它满足maxlen[i][j]=max{maxlen[i][j-1],maxlen[fa[i][j-1]][j-1]},这样就可以快速求出两点路径上最长边的边权……

本文参考自:https://blog.csdn.net/saramanda/article/details/54963914
本文参考自:http://www.mamicode.com/info-detail-2208455.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逃夭丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值