LCA 转 RMQ算法 【总结】

论坛 期权论坛 脚本     
已经匿名di用户   2022-7-2 21:50   1728   0

首先,在你看这个算法之前,要确保你理解了RMQ的 ST 算法。

但是不理解没关系啊,提供通道:生气 点我


声明:这是方便我以后复习用的,所以总结不是特别详细。


LCA - 最近公共祖先:在有根树中,两个节点u和v的公共祖先中距离最近的那个点。


上图理解:



如图,我们想要求出LCA(4,7),LCA(8,6),LCA(5,8)。


用LCA转RMQ思想 如此实现。


一:按从根DFS访问的顺序得到顶点序列vs[ i ] 和 对应的深度depth[ i ](两者下标是一一对应的)。对于每个顶点 v ,记其在vs中首次出现的下标为id[ v ]。

vs[ i ] 代表第i次DFS遍历的节点编号

depth[ i ]代表第i次DFS遍历的节点深度

id[ i ]代表节点 i 在vs中第一次出现的下标。

这里运用了时间戳dfs_clock 记录DFS次数。

代码实现:

#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#define MAXN 1010
#define MAXM 100000
using namespace std;
struct Edge
{
    int from, to, next;
};
Edge edge[MAXM];
int head[MAXN], edgenum;
int vs[MAXN<<1];//第i次DFS访问节点的编号
int depth[MAXN<<1];//第i次DFS访问节点的深度
int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标
int dfs_clock;//时间戳
int N, M;//点数 边数
int dp[MAXN<<1][20];//dp[i][j]存储depth数组  以下标i开始的,长度为2^j的区间里 最小值所对应的下标
void init()
{
    edgenum = 0;
    memset(head, -1, sizeof(head));
}
void addEdge(int u, int v)
{
    Edge E = {u, v, head[u]};
    edge[edgenum] = E;
    head[u] = edgenum++;
}
void getMap()
{
    int a, b;
    while(M--)
        scanf("%d%d", &a, &b),
        addEdge(a, b), addEdge(b, a);
}
void DFS(int u, int fa, int d)//当前遍历点以及它的父节点  遍历点深度
{
    id[u] = dfs_clock;
    vs[dfs_clock] = u;
    depth[dfs_clock++] = d;
    for(int i = head[u]; i != -1; i = edge[i].next)
    {
        int v = edge[i].to;
        if(v == fa) continue;
        DFS(v, u, d+1);
        vs[dfs_clock] = u;//类似 回溯
        depth[dfs_clock++] = d;
    }
}
void find_depth()
{
    dfs_clock = 1;
    memset(vs, 0, sizeof(vs));
    memset(id, 0, sizeof(id));
    memset(depth, 0, sizeof(depth));
    DFS(1, -1, 0);//遍历
}
void input()
{
    printf("下标:  ");
    for(int i = 1; i < dfs_clock; i++)
        printf("%d  ", i);
    printf("\n");
    printf("vs:    ");
    for(int i = 1; i < dfs_clock; i++)
        printf("%d  ", vs[i]);
    printf("\n");
    printf("depth: ");
    for(int i = 1; i < dfs_clock; i++)
        printf("%d  ", depth[i]);
    printf("\n");
    printf("下标:  ");
    for(int i = 1; i <= N; i++)
        printf("%d  ", i);
    printf("\n");
    printf("id:    ");
    for(int i = 1; i <= N; i++)
        printf("%d  ", id[i]);
    printf("\n");
}
int main()
{
    while(scanf("%d%d", &N, &M) != EOF)
    {
        init();
        getMap();
        find_depth();//DFS遍历整个树 求出所需要的信息
        input();//输出查找信息
    }
    return 0;
}

输入数据:

8 7
1 2
1 3
2 4
2 5
3 6
5 7
5 8

输出:(没有对齐)



模拟上图得如下结果:


i123456789101112131415
vs136312585752421
depth012101232321210


i12345678
id1621373108


二:LCA(u,v)就是访问u之后到访问v之前所经过顶点中离根最近的点。

假设id[ u ] <= id[ v ],那么有

LCA(u,v)= vs[ id[ u ] <= i <= id[ v ]中depth[ i ]最小的 i ]。


对于这个RMQ可以这样实现:


RMQ_init(dfs_clock - 1);
int dp[MAXN<<1][20];//dp[i][j]存储depth数组  以下标i开始的,长度为2^j的区间里 最小值所对应的下标
void RMQ_init(int NN)//预处理 区间最小值
{
    for(int i = 1; i <= NN; i++)
        dp[i][0] = i;
    for(int j = 1; (1<<j) <= NN; j++)
    {
        for(int i = 1; i + (1<<j) - 1 <= NN; i++)
        {
            int a = dp[i][j-1];
            int b = dp[i + (1<<(j-1))][j-1];
            if(depth[a] <= depth[b])//比较的下标所对应的值
                dp[i][j] = a;//更新下标
            else
                dp[i][j] = b;
        }
    }
}
int query(int L, int R)
{
    //查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标
    int k = 0;
    while((1<<(k+1)) <= R-L+1) k++;
    int a = dp[L][k];
    int b = dp[R - (1<<k) + 1][k];
    if(depth[a] <= depth[b])
        return a;//返回下标
    else
        return b;
}
int LCA(int u, int v)
{
    int x = id[u];//比较大小 小的当作左区间 大的当作右区间
    int y = id[v];
    if(x > y)
        return vs[query(y, x)];
    else
        return vs[query(x, y)];
}




练习:给你N个点、M条边和Q次查询(保证是一棵树)。查询a b 的LCA。


代码实现:


#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#define MAXN 1010
#define MAXM 100000
using namespace std;
struct Edge
{
    int from, to, next;
};
Edge edge[MAXM];
int head[MAXN], edgenum;
int vs[MAXN<<1];//第i次DFS访问节点的编号
int depth[MAXN<<1];//第i次DFS访问节点的深度
int id[MAXN];//id[i] 记录在vs数组里面 i节点第一次出现的下标
int dfs_clock;//时间戳
int N, M, Q;//点数 边数 查询数
int dp[MAXN<<1][20];//dp[i][j]存储depth数组  以下标i开始的,长度为2^j的区间里 最小值所对应的下标
void init()
{
    edgenum = 0;
    memset(head, -1, sizeof(head));
}
void addEdge(int u, int v)
{
    Edge E = {u, v, head[u]};
    edge[edgenum] = E;
    head[u] = edgenum++;
}
void getMap()
{
    int a, b;
    while(M--)
        scanf("%d%d", &a, &b),
        addEdge(a, b), addEdge(b, a);
}
void DFS(int u, int fa, int d)//当前遍历点以及它的父节点  遍历点深度
{
    id[u] = dfs_clock;
    vs[dfs_clock] = u;
    depth[dfs_clock++] = d;
    for(int i = head[u]; i != -1; i = edge[i].next)
    {
        int v = edge[i].to;
        if(v == fa) continue;
        DFS(v, u, d+1);
        vs[dfs_clock] = u;//类似 回溯
        depth[dfs_clock++] = d;
    }
}
void find_depth()
{
    dfs_clock = 1;
    memset(vs, 0, sizeof(vs));
    memset(id, 0, sizeof(id));
    memset(depth, 0, sizeof(depth));
    DFS(1, -1, 0);//遍历
}
void RMQ_init(int NN)//预处理 区间最小值
{
    for(int i = 1; i <= NN; i++)
        dp[i][0] = i;
    for(int j = 1; (1<<j) <= NN; j++)
    {
        for(int i = 1; i + (1<<j) - 1 <= NN; i++)
        {
            int a = dp[i][j-1];
            int b = dp[i + (1<<(j-1))][j-1];
            if(depth[a] <= depth[b])
                dp[i][j] = a;
            else
                dp[i][j] = b;
        }
    }
}
int query(int L, int R)
{
    //查询L <= i <= R 里面使得depth[i]最小的值 返回对应下标
    int k = 0;
    while((1<<(k+1)) <= R-L+1) k++;
    int a = dp[L][k];
    int b = dp[R - (1<<k) + 1][k];
    if(depth[a] <= depth[b])
        return a;
    else
        return b;
}
int LCA(int u, int v)
{
    int x = id[u];//比较大小 小的当作左区间 大的当作右区间
    int y = id[v];
    if(x > y)
        return vs[query(y, x)];
    else
        return vs[query(x, y)];
}
void solve()
{
    int a, b;
    while(Q--)
    {
        scanf("%d%d", &a, &b);
        printf("LCA(%d %d) = %d\n", a, b, LCA(a, b));
    }
}
int main()
{
    while(scanf("%d%d%d", &N, &M, &Q) != EOF)
    {
        init();
        getMap();
        find_depth();//DFS遍历整个树 求出所需要的信息
        RMQ_init(dfs_clock - 1);
        solve();
    }
    return 0;
}



测试结果:


8 7 3
1 2
1 3
2 4
2 5
3 6
5 7
5 8
4 7
8 6
5 8

输出:


LCA(4 7) = 2
8 6
LCA(8 6) = 1
5 8
LCA(5 8) = 5




毕竟是菜鸟,不好之处请见谅。当然若有不对的地方欢迎指正。


累死了。。。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:81
帖子:4969
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP