【模板】最近公共祖先(LCA)(倍增)
阅读须知:我认为读者已经掌握(或了解)了:
- 倍增思想
- 树(图)的基本概念及简单实现
- 存图与建图
- dfs
嗯,我们来看看最近公共祖先(LCA)的一种实现方式——倍增。
最近公共祖先
话说什么是最近公共祖先呢?emmm……大家如果知道树的话,应该就知道父亲节点与儿子节点了吧,那么祖先就是父亲的父亲的父亲的……;总之在同一条树链上,若x的深度小于y的深度,则称x是y的祖先。公共祖先,顾名思义就是两个点共同拥有的祖先;但是若x是y的祖先,则他们的共同祖先是x(很奇怪是不是)。最近公共祖先可以依字面意思理解,即“最近的”公共祖先。下面附图说明~
【图1】
如上图4和7的公共祖先有5、10,最近公共祖先为5;
3与20的公共祖先和最近公共祖先都是10;
2和3的公共祖先是3、5、10,最近公共祖先是3。
怎么样,对最近公共祖先是不是有些了解了。
下面我们再来看两张图~
【图2】
【图3】
请读者们仔细比对上面两张图,会发现,选择不同的根节点会使得两点之间有着不同的最近公共祖先。
倍增
关于实现LCA,想必大家都有暴力的思路,不断询问x与y的父亲节点,直到发现他们询问到相同的父亲节点位置,这当然会超时!所以,我们跳着查找【滑稽】,这就是倍增的思想。
我们使用一个数组lst[i][j]表示从i这个点向上跳2^j次所对应的点,即节点i的第2^j个父亲节点的编号。比如说上面第二个图中lst[5][0]=2,lst[5][1]=1。
请读者们细细体会下面的转化【很重要】(可以画一颗树试一试):
lst[son][i + 1] = lst[ lst[son][i] ][i]
倍增LCA的实现
1.建图(建议使用链式前向星)
2.预处理,更新lst数组
3.查询
下面分条作答
1.建图
链式前向星存图【不了解的话建议百度】
//建树一般不用存边权,建树一般要存双向边
struct EDGE
{
int to; //到达的点
int next; //上一条边
int w; //边权
}edge[2 * max_data]; //双向边
int edge_size = 0; //前向星数组模拟指针
int head[max_data]; //起点
void add(int u, int v, int w)
{
edge[++edge_size].next = head[u];
edge[edge_size].to = v;
edge[edge_size].w = w;
head[u] = edge_size;
}
2.预处理,更新lst(倍增表)
思想:利用上面说的重要式子更新lst。从根节点出发,遍历每一个点,在遍历时利用父子关系【滑稽】更新lst。
注意:要使根节点深度设为1(否则会使后面的st可能变为-1)。
int vis[max_data]; //防止一个点被遍历多次【重要】
int lst[max_data][33]; //倍增表
int deep[max_data]; //记录深度
int main()
{
deep[x] = 1; //根节点深度设为1
dfs(x);
}
void dfs(int x) //从根节点开始遍历
{
vis[x] = 1;
for(int i = head[x]; i; i = edge[i].next) //遍历每一个与x相连的点
{
int temp = edge[i].to; //下一个点即当前点的儿子节点
if(!vis[temp])
{
lst[temp][0] = x; //儿子上跳一位既是父亲
deep[temp] = deep[x] + 1; //儿子比父亲深度加1
for(int j = 0, last = x; lst[last][j]; last = lst[last][j], j++) lst[temp][j + 1] = lst[last][j]; //这里既是用那个重要的式子更新lst
dfs(temp); //继续遍历
}
}
}
也可以不把那个重要式子加进dfs里,也可以dfs后单独再建倍增表(ST表)
void increase__pretreatment()
{
for(register int j = 1; j <= 19; ++j)//register为设置寄存器变量,可以比较玄学地加速程序(不建议随便使用)
for(register int i = 1; i <= n; ++i)
f[i][j] = f[f[i][j - 1]][j - 1]; //倍增预处理
}
3.查询
终于到倍增大显身手的时候了,上面我们说到逐层查找会很慢,所以我们跳着查,通过倍增法达到快速上搜的目的。
首先,我们找到一个深度较大的点,利用倍增快速上搜到另一个点相同的深度。
然后,让两个点同时倍增快速上搜,直到搜到他们最近公共祖先的儿子为止。
注意,倍增快速上搜时从较大跨度逐渐向较小跨度搜,这样可以保证搜索到正确答案。
int lca(int x, int y)
{
if(deep[x] < deep[y]) swap(x, y); //找最深的点
int st; //记录倍增搜索的最大跨度
for(st = 0; (1 << st) <= deep[x]; st++); st--;//查找最大跨度(位运算 x << y 表示x * (2 ^ y))
for(int i = st; i >= 0; i--) //调整较深点到较潜点深度
if(deep[lst[x][i]] >= deep[y])
x = lst[x][i]; //倍增跳跃
if(x == y) return x;
for(int i = st; i >= 0; i--)
if(lst[x][i] != lst[y][i]) //if会过滤掉倍增跳跃超过所求点深度的点(当然等于也被无情地筛去了),也会在没有跳到lca前实现不断跳跃,最后保证找到lca的儿子节点(lca被筛除)
x = lst[x][i], y = lst[y][i]; //跳吧!!!
return lst[x][0]; //最后找到的是儿子,所以返回父亲
}
完整实战模板
题目描述
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
输入格式:
第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。
(数据保证可以构成树)。
接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。
输出格式:
输出包含M行,每行包含一个正整数,依次为每一个询问的结果。 时空限制:1000ms,128M
#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<map>
#include<vector>
#include<algorithm>
#include<stack>
#include<queue>
#define by Mashiro_ylb
#define Time 2017\10\30
using namespace std;
const int max_data = 500007;
struct EDGE
{
int to;
int next;
}edge[2 * max_data];
int edge_size = 0;
int head[max_data];
int n, m, s;
int vis[max_data];
int lst[max_data][33];
int deep[max_data];
template<class T>void read(T &x)
{
int f = 0; x = 0; char ch = getchar();
while(ch < '0' || ch > '9') f |= (ch == '-'), ch = getchar();
while(ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
x = f? -x : x;
}
template<class T>void write(T x)
{
if(x < 0) x = -x, putchar('-');
if(x > 9) write(x / 10);
putchar(x % 10 + '0');
}
void add(int u, int v)
{
edge[++edge_size].next = head[u];
edge[edge_size].to = v;
head[u] = edge_size;
}
template<class T>T change(T &x, T &y){int w = x; x = y; y = w;}
void dfs(int s);
void lca();
void init();
void work();
int main()
{
// freopen("in.txt","r",stdin);
init();
work();
return 0;
}
void init()
{
read(n);read(m);read(s);
for(int i = 1; i <= n - 1; i++)
{
int u, v;
read(u);read(v);
add(u, v);
add(v, u);
}
deep[s] = 1;
dfs(s);
}
void dfs(int x)
{
vis[x] = 1;
for(int i = head[x]; i; i = edge[i].next)
{
int temp = edge[i].to;
if(!vis[temp])
{
lst[temp][0] = x;
deep[temp] = deep[x] + 1;
for(int j = 0, last = x; lst[last][j]; last = lst[last][j], j++) lst[temp][j + 1] = lst[last][j];
dfs(temp);
}
}
}
int lca(int x, int y)
{
if(deep[x] < deep[y]) change(x, y);
int st;
for(st = 0; (1 << st) <= deep[x]; st++); st--;
for(int i = st; i >= 0; i--)
if(deep[lst[x][i]] >= deep[y])
x = lst[x][i];
if(x == y) return x;
for(int i = st; i >= 0; i--)
if(lst[x][i] != lst[y][i])
x = lst[x][i], y = lst[y][i];
return lst[x][0];
}
void work()
{
int x, y;
for(int i = 1; i <= m; i++)
{
read(x);read(y);
write(lca(x, y));
putchar(10);
}
}
嗯,还是比较简单的一种算法,大家加油吧。
!注意:本篇博文以模板为主,对倍增什么的讲解并不细致,建议大家优先深入了解倍增(还有其他不细致讲解)的具体思想及其具体实现,谢谢。
博文修改记录:
- 2017.11.8 修改遗漏点(初始深度设为1)
- 2017.11.8 代码优化——if(deep[x] - (1 << st) >= deep[y]) -> if(deep[lst[x][i]] >= deep[y])——