zoukankan      html  css  js  c++  java
  • LCA RMQ+ST表学习笔记

    RMQ

    RMQ问题:在给定的一个长度位N的区间中,有M个询问,每次询问给出区间[L,R],求出区间段元素的

    最大值/最小值。对于RMQ问题很容易想到遍历的做法,将区间[L,R]中的元素遍历一遍,即可寻找到

    最大/最小值,但当区间长度较大,询问次数较多,就会耗费大量的时间。RMQ问题可以用线段树和ST

    表两种做法,下面介绍ST表。

    ST表法:

    ST表是用来解决RMQ问题非常高效的方法,通过O(nlong)的预处理之后,可以在O(1)时间

    内找到所要的答案。其中预处理就是用到了动态规划的思想。

    定义F(i,j)表示以i为下标的元素为起点,区间长度为2^j的区间最值 此时区间范围是[i,i+2j-1-1]

    预处理:容易发现初始状态为 F(i,0),此时该值表示区间[i,i]的最值,F(i,0)=ai;

    状态转移:可以将[i,j]分成长度为2j-1的两段,一段为[i,i+2j-1-1],另一端为[i+2j−1,i+2j-1]则

    F(i,j)=max{f(i,j-1),f(i+2j-1,j-1)}

    代码:

    void ST(int n)
    {
        for(int i=1;i<=n;i++)                            //预处理
            dp[i][0]=i;            
            
        for(int j=1;(1<<j)<=n;j++)
            for(int i=1;i+(1<<j)-1<=n;i++)
            {
                int a=dp[i][j-1];int b=dp[i+(1<<(j-1))][j-1];
                dp[i][j]=a<b?a:b;
            }    
    }
    View Code

    查询操作:若查询[l,r]区间,在预处理时,每个状态对应的区间长度均为2i,给定查询区间的长度不一定为2i

    所以可以将查询区间分成两小区间,两个小区间要覆盖大区间,并且长度相等为2i,(两个区间可以重叠)。

    计算时小区间长度为大区间长度取2为底的对数 k = log(l-r+1)则待查区间[l,r]可分为[l,l+2k-1]  [r-2k+1,r],

    对应着F(l,k)和F(r-2^k+1,k),此时比较两个的值,取答案即可。

    代码:

    int RMQ(int l,int r)
    {
        int k=0;
        while(1<<(k+1)<=r-l+1)
            k++;
        int a=dp[l][k],b=dp[r-(1<<k)+1][k];
        return a<b?a:b;
    }
    View Code

    LCA

    定义:对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深

    度尽可能大(百度百科)。做法很多,这里结束以上文为基础的做法

    DFS+ST表法:

    思想:将树看成一个无向图,u和v的公共祖先一定在u与v之间的最短路径上,而路径中深度最小的结点,即为u,v的最近公共祖先。

    算法步骤:

    (1) DFS

    a:从树的根开始,将树看成一个无向图进行深度优先遍历,记录下每次到达的顶点,第一个顶点为树根root,

        每经过一条边都记录它的端点,每条边都恰好经过两次.用数组ver记录结点。

    b:记录first数组和deep数组,first数组记录在深度优先遍历时结点第一次出现的位置。Deep数组记录结点的深度

    void dfs(int u,int dep)
    {
        vis[u]=true;
        ver[++tot]=u;
        first[u]=tot;
        deep[tot]=dep;    
        for(int i=head[u];i!=-1;i=edge[i].next)
        {
            int v=edge[i].to;
            if(!vis[v])
            {
                dfs(v,dep+1);
                ver[++tot]=u;
                deep[tot]=dep;
            }
        
        }
    } 
    View Code

    可以发现当我们通过深度优先遍历记录下结点后,当我们要查询结点u,v时,我们可以在结点的数组

    中找到u结点第一次出现的位置和v结点第一次出现的位置,而他们位置之间的结点便是u到v的DFS顺序

    ,虽然其中可能包含u或v的后代,但其中深度最小的还是u和v的最近公共祖先。因此可以用ST表记录与

    结点数组相对应的深度序列的区间最小值下标,将lca转化为RMQ问题。

    void ST(int n)
    {
        for(int i=1;i<=n;i++)
            dp[i][0]=i;
            
        for(int j=1;(1<<j)<=n;j++)
            for(int i=1;i+(1<<j)-1<=n;i++)
            {
                int a=dp[i][j-1];int b=dp[i+(1<<(j-1))][j-1];            //记录其中结点深度最小的结点的位置
                dp[i][j]=deep[a]<deep[b]?a:b;
            }    
    }
    View Code

    寻找LCA(u,v)时,先寻找first[u],first[v],将[first[u],first[v]]间的最小值的deep找出,该值下标所对应的结点即为LCA(u,v)。

    即当first[u]>first[v]时,LCA(T,u,v)=RMQ(deep,R[v],R[u]),否则LCA(T,u,v) = RMQ(deep,R[u],R[v]).

    int RMQ(int l,int r)
    {
        int k=0;
        while(1<<(k+1)<=r-l+1)
            k++;
        int a=dp[l][k],b=dp[r-(1<<k)+1][k];
        return deep[a]<deep[b]?a:b;
    }
    int LCA(int u,int v)
    {   
        int x=first[u],y=first[v];
        if(x>y)swap(x,y);
        int res=RMQ(x,y);
        return ver[res];
    }
    View Code

    示例:

    数组下标:       1  2  3  4  5  6  7  8  9  10  11  12  13

    遍历序列:       A  B  D  B  E  F  E  G  E  B   A   C   A

    结点在树中深度:      1  2  3   2  3  4  3   4   4   2   1   2   1

    假设查询LCA(F,C)

    1、查询F,C在序列中第一次出现的位置first[F] = 6, first[C]=12

    2、去结点数组[6,12]的序列,查询与之对应的深度deep数组的最小值即

    查询< 4, 3 ,4 ,4,2 ,1,2>,最小值为1,对应下标为11,即结点A,LCA(F,C)=A.

    查询操作即为区间最小值,转化为RMQ即可。

    例题: POJ - 1330 Nearest Common Ancestors

    代码:

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    using namespace std;
    const int MAX=10009;
    int T,n,a,b;
    int head[MAX],cnt=0;
    int tot=0;
    int dp[MAX*2][25];                //ST表 
    int deep[MAX*2];                //记录节点深度 
    int ver[MAX*2];                    //记录节点编号 
    int first[MAX];                    //记录点第一次出现的位置 
    bool vis[MAX];    
    bool isroot[MAX];                //判断根节点的数组 
    struct Edge{            
        int to,next;
    }edge[MAX*2];
    inline void add(int u,int v)
    {
        edge[cnt].to=v;
        edge[cnt].next=head[u];
        head[u]=cnt++;
    }
    void dfs(int u,int dep)
    {
        vis[u]=true;            //访问过该节点 
        ver[++tot]=u;            //将该节点记录在ver中 
        first[u]=tot;            //记录结点u第一次出现的位置 
        deep[tot]=dep;            //记录深度 
        for(int i=head[u];i!=-1;i=edge[i].next)
        {
            int v=edge[i].to;
            if(!vis[v])
            {
                dfs(v,dep+1);
                ver[++tot]=u;
                deep[tot]=dep;
            }
        
        }
    } 
    void ST(int n)
    {
        for(int i=1;i<=n;i++)                //初始化 
            dp[i][0]=i;
            
        for(int j=1;(1<<j)<=n;j++)
            for(int i=1;i+(1<<j)-1<=n;i++)
            {
                int a=dp[i][j-1];int b=dp[i+(1<<(j-1))][j-1];        //记录其中结点序列深度最小的结点的编号 
                dp[i][j]=deep[a]<deep[b]?a:b;
            }    
    }
    int RMQ(int l,int r)
    {
        int k=0;
        while(1<<(k+1)<=r-l+1)                    //求区间长度以二为底的对数 
            k++;
        int a=dp[l][k],b=dp[r-(1<<k)+1][k];
        return deep[a]<deep[b]?a:b;
    }
    int LCA(int u,int v)
    {
        int x=first[u],y=first[v];
        if(x>y)swap(x,y);
        int res=RMQ(x,y);
        return ver[res];
    }
    void init()
    {
        memset(head,-1,sizeof(head)),cnt=0;tot=0;
        memset(isroot,true,sizeof(isroot));
        memset(vis,false,sizeof(vis));
        memset(dp,0,sizeof(dp));
        memset(deep,0,sizeof(deep));
        memset(first,0,sizeof(first));
        memset(ver,0,sizeof(ver));
    } 
    int main()
    {
        scanf("%d",&T);
        while(T--)
        {
            scanf("%d",&n);
            init();
            for(int i=1;i<n;i++)
            {
                scanf("%d%d",&a,&b);
                isroot[b]=false;
                add(a,b);
                add(b,a);
            }
            int root;
            for(int i=1;i<=n;i++)
            {
                if(isroot[i])
                {
                    root=i;break;
                }
            }
            dfs(root,1);
            ST(2*n-1);
            scanf("%d%d",&a,&b);
            printf("%d
    ",LCA(a,b));
        }
        return 0;
    }
    View Code

    参考:

    http://dongxicheng.org/structure/lca-rmq/

    https://www.cnblogs.com/YSFAC/p/7189571.html

    如有错误,欢迎指出,谢谢~

  • 相关阅读:
    软件测试笔记(二):软件测试流程
    关于Kotlin中日志的使用方法
    Github Pages+Gridea设置DisqusJS评论
    软件测试笔记(一):软件测试概论
    CVPR2021| TimeSformer-视频理解的时空注意模型
    经典论文系列 | Group Normalization & BN的缺陷
    经典论文系列 | 重新思考在ImageNet上的预训练
    CVPR2021 | 华为诺亚实验室提出Transformer in Transformer
    经典论文系列| 实例分割中的新范式-SOLO
    我们真的需要模型压缩吗
  • 原文地址:https://www.cnblogs.com/LjwCarrot/p/9971798.html
Copyright © 2011-2022 走看看