RMQ问题与LCA问题

本文详细介绍了RMQ(Range Minimum/Maximum Query)问题及其解决方法,包括直接遍历查询、线段树和ST算法。重点阐述了ST算法的实现原理、初始化过程和查询方法,同时提供了一个变种思路,利用堆排序思想来求解连续k个数的最值。此外,还简要介绍了LCA(Lowest Common Ancestor)问题的离线和在线算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RMQ

RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。数组大小为n,查询次数为q。

解决办法:

1.直接遍历查询,复杂度O(q*n)

2.线段树,复杂度O(n+q*logn)

3.ST算法(Sparse Table),复杂度O(nlogn+q)




 ST算法中用mini[i][j]、maxi[N][MAXN]表示[i,i+2^j-1]这个区间内的最值,询问到[a,b]区间的最大值时答案就是max(m[a][k], m[b-2^k+1][k]),其中k是满足2^k<=b-a+1(即长度)的最大的k,即k=[ln(b-a+1)/ln(2)] 

初始化(动态规划):m[i,j]=max(m[i][j-1],m[i+2^(j-1)][j-1])

 

#define N 50001

#define MAXN 16

int mini[N][MAXN],maxi[N][MAXN];

int max(int x,int y){return x>y?x:y;}

int min(int x,int y){return x>y?y:x;}

int bitnum(int v)

{

      int num=0;

      while(v=v>>1)

          num++;

      return num;

}

void init(int n,int h[])

{

      int i,j,m=bitnum(n);

      for(i=1;i<=n;i++)

          mini[i][0]=h[i];

      for(i=1;i<=n;i++)

          maxi[i][0]=h[i];

      int k=1;

      for(j=1;j<=m;j++)

      {

          for(i=1;i+k<=n;i++){

               maxi[i][j]=max(maxi[i][j-1],maxi[i+k][j-1]);

               mini[i][j]=min(mini[i][j-1],mini[i+k][j-1]);

          }

          k=k<<1;

      }

}

void query(int a,int b)

{

      int k=bitnum(b-a+1);

      int low=min(mini[a][k],mini[b-(1<<k)+1][k]);

      int up=max(maxi[a][k],maxi[b-(1<<k)+1][k]);

      return up-low;

}



 变种:求从左至右连续k个数的最值

一种新思路:利用堆排序思想O(nlogn)

struct Node

{

      int v,index;

}nod,node[N];

int a[N];

void add(int n)

{

      while(n>1)

      {

          if(node[n].v>=node[n/2].v)

               break;

          nod=node[n];

          node[n]=node[n/2];

          node[n/2]=nod;

          n/=2;

      }

}

void restore(int n)

{

      int j,k;

      j=1;

      while(j<=n/2)

      {

          k=2*j;

          if(k+1<=n&&node[k+1].v<node[k].v)

               k++;

          if(node[j].v<=node[k].v)

               break;

          nod=node[k];

          node[k]=node[j];

          node[j]=nod;

          j=k;

      }

}

/*返回最小值*/

for(i=1;i<=k;i++){

      node[i].v=a[i];

      node[i].index=i;

      add(i);

}

printf("%d",node[1].v);

l=k;

for(i=k+1;i<=n;i++)

{

      l++;

      node[l].index=i;

      node[l].v=a[i];

      add(l);

      while(node[1].index<i-k+1){

          node[1]=node[l];

          l--;

          restore(l);

      }

      printf(" %d",node[1].v);

}


LCA 最近公共祖先问题

(Least Common Ancestors)对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。

类似问题:

1. 求树中任意两点的距离:两点到根节点的距离和-公共祖先到根节点距离*2

2. 以一个节点x为根求另一节点y的父节点:x和y有共同的祖先z,y的父节点就是解;x和y的共同祖先为x,y的父节点就是解;x和y的共同祖先为y,x到y的路径的y的前一个节点(y的子节点)为解。


1. 离线算法Tarjan


利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n+Q)算法,这里Q表示询问的次数。Tarjan算法基于深度优先搜索的框架,对于新搜索到 的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询 问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所 有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于 进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v 所在集合的祖先。

参考: http://poj.org/showsource?solution_id=4237726

代码存在问题,需要以后进一步斟酌。

vector<int> v[N];

bool tag[N];

int d[2*N],start[N],p[2*N],mini[2*N][16];

int cnt;

void dfs(int i,int deep)

{

      d[cnt]=deep;

      start[i]=cnt;

      p[cnt++]=i;

      int j;

      for(j=0;j<v[i].size();j++)

      {

          dfs(v[i][j],deep+1);

          d[cnt]=deep;

          p[cnt++]=i;

      }

}

int bitnum(int v)

{

      int num=0;

      while(v=v>>1)

          num++;

      return num;

}

void init(int n)

{

      int i,j,m=bitnum(n);

      for(i=1;i<=n;i++)

          mini[i][0]=i;

      int k=1;

      for(j=1;j<=m;j++)

      {

          for(i=1;i+k<=n;i++){

               if(d[mini[i][j-1]]<=d[mini[i+k][j-1]])

                    mini[i][j]=mini[i][j-1];

               else

                    mini[i][j]=mini[i+k][j-1];

          }

          k=k<<1;

      }

}

void query(int a,int b)

{

      int low,k=bitnum(b-a+1);

      if(d[mini[a][k]]<=d[mini[b-(1<<k)+1][k]])

          low=mini[a][k];

      else

          low=mini[b-(1<<k)+1][k];

      printf("%d\n",p[low]);

}


 2.在线算法 倍增法  

 

复杂度:每次询问O(logN),总复杂度O(nlogn+qlogn)

思路:d[i]表示 i节点的深度, p[i,j] 表示 i 的 2^j 倍祖先。那么就有一个递推式子 p[i,j]=p[p[i,j-1],j-1]。这样子一个O(NlogN)的预处理求出每个节点的 2^k 的祖先。然后对于每一个询问的点对a, b的最近公共祖先就是:先判断是否 d[a] > d[b] ,如果是的话就交换一下(保证 a 的深度小于 b 方便下面的操作)然后把b 调到与a 同深度, 同深度以后再把a, b 同时往上调(dec(j)) 调到有一个最小的j 满足p[a,j]!=p[b,j] (a b 是在不断更新的), 最后再把 a, b 往上调 (a=p[a,0], b=p[b,0]) 一个一个向上调直到a = b, 这时 a or b 就是他们的最近公共祖先。

 

一个初始化的算法:若祖先不存在p[i,j]=-1

vector<int> v[N];
int p[N][M],d[N];
void getparent(int u,int r,int l)
{
    d[u] = l;
    int xx = r;
    int i = 0;
    while(xx != -1)
    {
        p[u][i++] = xx;
        xx = p[xx][i-1];
    }
    for(i = 0;i < v[u].size();i++){
        if(v[u][i] != r){
            getparent(v[u][i],u,l+1);
        }
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值