【讲解+模板】最近公共祖先(LCA)(倍增)

【模板】最近公共祖先(LCA)(倍增)

阅读须知:我认为读者已经掌握(或了解)了:

  1. 倍增思想
  2. 树(图)的基本概念及简单实现
  3. 存图与建图
  4. dfs

嗯,我们来看看最近公共祖先(LCA)的一种实现方式——倍增。

最近公共祖先

话说什么是最近公共祖先呢?emmm……大家如果知道树的话,应该就知道父亲节点与儿子节点了吧,那么祖先就是父亲的父亲的父亲的……;总之在同一条树链上,若x的深度小于y的深度,则称x是y的祖先。公共祖先,顾名思义就是两个点共同拥有的祖先;但是若x是y的祖先,则他们的共同祖先是x(很奇怪是不是)。最近公共祖先可以依字面意思理解,即“最近的”公共祖先。下面附图说明~
1【图1】
如上图4和7的公共祖先有5、10,最近公共祖先为5;
3与20的公共祖先和最近公共祖先都是10;
2和3的公共祖先是3、5、10,最近公共祖先是3。
怎么样,对最近公共祖先是不是有些了解了。
下面我们再来看两张图~
2【图2】

3【图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);
      }
}

嗯,还是比较简单的一种算法,大家加油吧。

!注意:本篇博文以模板为主,对倍增什么的讲解并不细致,建议大家优先深入了解倍增(还有其他不细致讲解)的具体思想及其具体实现,谢谢。

博文修改记录:

  1. 2017.11.8 修改遗漏点(初始深度设为1)
  2. 2017.11.8 代码优化——if(deep[x] - (1 << st) >= deep[y]) -> if(deep[lst[x][i]] >= deep[y])——

这里写图片描述

猜你喜欢

转载自blog.csdn.net/Mashiro_ylb/article/details/78399025
今日推荐