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