Localhost8080

知行合一,自强不息

 

RMQ问题与LCA问题

topcoder上关于RMQ问题的精彩讲解

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=lowestCommonAncestor

一、区间最小/ 最大查询(Range Minimum/Maximum Query) :RMQ 问题
RMQ作为学习Suffix Array的基本知识之一,我想在写SA总结之前写下这一部分的知识。

RMQ问题可以算是经典问题中的经典,目的就是希望用较短的时间完成求区间最小值的操作。

方法有很多种,最简单的直接线性统计的效率极其低下,也比较白痴,差不多都应该会。

下面讲下几个比较好的RMQ问题的算法。

方法一:线段树

线段树作为维护区间性质的最佳利器,绝对是不错的选择。写起来可以递归,非常的方便。

但是他也是有缺点的,最大的问题就是虽然说是O(nlogn)-O(logn)的时间复杂度中的常数比较大。从实际运行效率上看不是很好。

我前面的日志里面写过一些线段树的题,线段树的用法大同小异,也比较好学,这里不再赘述。

方法二:RMQ的ST算法

ST算法是一个很不错的算法,复杂度为O(nlogn)-O(1)。这种算法运用的主要思想是倍增思想。

倍增思想的核心就是,通过递推使得结论成倍地提高。

这里我们定义这样一个数组f[i,j]表示从i开始向后数2j个元素的RMQ

递推方程就是

           f[i,j]:=min(f[i,j-1],f[i+1 shl (j-1),j-1]);

边界:f[i,0]:=a[i];

这样我们就可以用这样的式子来解决RMQ问题:

设k:=trunc(ln(j-i+1)/ln(2))

          RMQ(i,j):=min(f[i,k],f[j-(1 shl k)+1,k]);

对于某一些特殊的RMQ问题还有O(n)-O(1)的算法,方法在于分成长度特殊的段,然后再用这些段RMQ。这是将LCA问题转化为RMQ问题的途径。

方法三:笛卡尔树

这个方法的精髓在于将RMQ问题转化为LCA问题。

笛卡尔树的定义是,对于一个数组A来说,笛卡尔树的根的编号是这个数组的最小值的所在的编号k。他的左子树就是在数组A[1..k-1]上的一棵笛卡尔树,右子树是一棵在数组A[k+1..n]的一棵笛卡尔树。

这样RMQ(A,i,j)就被转化成了LCA(T,i,j)。

因为LCA问题离线Tarjan算法的复杂度是O(n)的,所以这个问题最终的复杂度在O(n)。

我的理解就是理论性比较强,并没有什么实际意义。

缺点是不支持在线提问。

以上就是RMQ问题的几种经典方法,如果想要透彻的理解好线段树和倍增法,还是需要一些题目的磨练。

//Toj: 2762

[ 描述] 已知长度为L 的数列A ,询问区间[l, r] 中的最值。

若询问的次数较少,可以用线性的复杂度来查询,但如果询问的次数过多且L 过大,那么复杂度就会很高。所以需要更快速的查找方法。

ST 算法:预处理O(nlogn) ,查询指定区间的最值O(1)

    把待求区间[l, r] 分为两段长为len 的区间,左边一段为[l, l+len-1] ,右边一段为[r-len+1, r] ,len 必须使得两段区间覆盖待求区间。

    设所求数组为W :

    那么,所求最小值就是两个区间的最小值的最小值:

即Min(Min{W[i], l<=i<=l+len-1}, Min{W[j], r-len+1<=j<=r})

若都在预先处理中先求得两个区间的最小值,则每次查询的复杂度都是O(1) 。

预处理:(DP )

    设dp[i][j] 表示从第i 个数起连续2^j 个数中的最值。

    dp[i][0]=a[i] 1<=i<=N

    dp[i][j]=Min(dp[i][j-1], dp[i+2^(j-i)][j-1])

求最值:把区间[l, r] 分成两个长度为2^n 的区间。

    为使区间被分解后,长度为2^n ,区间部分可重叠,但不可越过[l, r] 。

    例如 [3, 9] à [3, 6]+[6, 9] 即:dp[3, 2] 和dp[6, 2]

#define Max(a, b) (a)>(b)?(a):(b)   
#define Min(a, b) (a)<(b)?(a):(b)    

int Dmin[50005][20], Dmax[50005][20];      
// 最小值和最大值的DP 矩阵    
int num[50005];     // 记录区间中的数据    
void Init(int N)        // 计算DP 矩阵 DP 规则如上    
{    
    
int i, j, M;    
    
for (i=1; i<=N; i++)    
    
{    
        Dmin[i][
0]=num[i];    
        Dmax[i][
0]=num[i];    
    }
    
    
for (i=1, M=1; M<=N; i++, M*=2)    
        
for (j=N; j>0; j--)    
        
{    
            Dmax[j][i]
=Dmax[j][i-1];    
            
if (j+(1<<(i-1))<=N) Dmax[j][i]=Max(Dmax[j][i], Dmax[j+(1<<(i-1))][i-1]);    
            Dmin[j][i]
=Dmin[j][i-1];    
            
if (j+(1<<(i-1))<=N) Dmin[j][i]=Min(Dmin[j][i], Dmin[j+(1<<(i-1))][i-1]);    
        }
    
        
return;    
}
    
int RMQ(int l, int r)           // 计算[l, r] 区间中的最值    
{    
    
int i, j, k;    
    k
=r-l+1;                // 记录区间中数字的个数    
    for (i=0, j=12*j<=k; j<<=1, i++);    
    
int a=Max(Dmax[l][i], Dmax[r-j+1][i]);    
    
int b=Min(Dmin[l][i], Dmin[r-j+1][i]);    
    
return a-b;         // 返回a 为最大值返回b 为最小值    
}
 

 

注:Dmin 与Dmax 用前清零。

将RMQ 拓展到二维状况时,依然使用ST 算法先进行预处理,再进行查询。其中可以有两种做法(以取最大值为例),对于数字矩阵num[1..M][1..N] ,取其中一个子矩阵的最大值。第一种方法:Dmax[x][y][i][j] 表示对于点(x, y) 为左上点,(x+2^i-1, y+2^j-1) 为右下点的子矩阵的最大值。在询问时,将待查询矩阵一分为四取最大值即可。

 toj 3375

#define max(a, b) (a)>(b)?(a):(b)    

int num[305][305];      // 记录数字矩阵    
int Dmax[305][305][10][10];     // 记录DP 矩阵    
inline int Max(int a, int b, int c, int d)    
// 取最大值    
{    
    
return max(max(a, b), max(c, d));    
}
    
void Init(int M, int N)    
// 预处理 M 行N 列    
{    
    
int i, j, x, y;    
    
for (i=1; i<=M; i++)    
        
for (j=1; j<=N; j++)    
            Dmax[i][j][
0][0]=num[i][j];    
    
for (i=0; (1<<i)<=M; i++)    
        
for (j=0; (1<<j)<=N; j++)    
            
if (i!=0||j!=0)    
                
for (x=1; x+(1<<i)-1<=M; x++)    
                    
for (y=1; y+(1<<j)-1<=N; y++)    
                        
if (i==0
                            Dmax[x][y][i][j]
=max(Dmax[x][y][i][j-1], Dmax[x][y+(1<<(j-1))][i][j-1]);    
                        
else Dmax[x][y][i][j]=max(Dmax[x][y][i-1][j], Dmax[x+(1<<(i-1))][y][i-1][j]);    
                    
return;    
}
    
int RMQ(int x1, int y1, int x2, int y2)    
// 查询以(x1, y2) 为左上角,(x2, y2) 为右下角的子矩阵的最大值    
{    
    
int xi, yi, xj, yj, xk=x2-x1+1, yk=y2-y1+1;    
    
for (xi=0, xj=12*xj<=xk; xj<<=1, xi++);    
    
for (yi=0, yj=12*yj<=yk; yj<<=1, yi++);    
    
return Max(Dmax[x1][y1][xi][yi], Dmax[x1][y2-yj+1][xi][yi], Dmax[x2-xj+1][y1][xi][yi], Dmax[x2-xj+1][y2-yj+1][xi][yi]);    
}
   

 

第二种方法:是Dmax[x][y][i] 记录以(x, y) 为左上点,(x+2^i-1, y+2^i-1) 为右下点的正子矩阵的最大值。在询问时,也需要将待查询矩阵分为若干正子矩阵,取最大值即可。

第二种方法由于比第一种方法在Dmax 上少了一个维度,所以大大的节省了空间。

#define max(a, b) (a)>(b)?(a):(b)    
int num[305][305];    
int Dmax[305][305][9];    
inline 
int Max(int a, int b, int c, int d)    
{    
    
return max(max(a, b), max(c, d));    
}
    
void Init(int M, int N)    
{    
    
int i, j, x, y;    
    
for (i=1; i<=M; i++)    
        
for (j=1; j<=N; j++)    
            Dmax[i][j][
0]=num[i][j];    
    
for (i=1; (1<<i)<=M&&(1<<i)<=N; i++)    
        
for (x=1; x+(1<<i)-1<=M; x++)    
            
for (y=1; y+(1<<i)-1<=N; y++)    
                Dmax[x][y][i]
=Max(Dmax[x][y][i-1], Dmax[x][y+(1<<(i-1))][i-1], Dmax[x+(1<<(i-1))][y][i-1], Dmax[x+(1<<(i-1))][y+(1<<(i-1))][i-1]);    
    
return;    
}
    
int RMQ(int x1, int y1, int x2, int y2)    
{    
    
int x, y, i, l, d, res=0;    
    
if (x2-x1>y2-y1) d=y2-y1;    
    
else d=x2-x1;    
    
for (l=0, i=12*i<=d; i<<=1, l++);    
    
for (x=x1; x<=x2; x=x+(1<<l))    
    
{    
        
if (x+(1<<l)-1>x2) x=x2-(1<<l)+1;    
        
for (y=y1; y<=y2; y=y+(1<<l))    
        
{    
            
if (y+(1<<l)-1>y2) y=y2-(1<<l)+1;    
            res
=max(res, Dmax[x][y][l]);    
        }
    
    }
    
    
return res;    
}

 

一、 最近公共祖先( Lowest Common Ancestors ) :LCA 问题

//Toj: 3361 1051

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

    解决LCA 主要是用最经典的Tarjan 离线算法,需要用到并查集来辅助。

1 、在并查集中建立仅包含x 结点的集合,即Fa[x]=x;

2 、处理x 的所有孩子,处理完每个孩子后,令Fa[ 孩子]=x ,即将孩子和x 所在集合合并;

3 、全部孩子处理完后,将x 标记为处理结束;

4 、处理所有与x 相关的询问。对于每个询问LCA(x, y) ,若y 已被标记,则记录下LCA(x, y)=Find(y) ,即y 所在集合的代表元素。

[ 模板] ( 以3361 为例)

在处理询问的时候需要建立关联序列表,另外调用LCA 时候必须调用根结点。

在查询的时候根据题意修改。

const int Maxn=10005;    
struct Node    
{    
    
int x, y, val;    
}
 Q[1000005];    
int N, M, C;    
int Fa[Maxn];    
int Check[Maxn], Path[Maxn];    
map 
<intint> Map[Maxn];    
vector 
<int> Mark[Maxn];    
int Find(int x)    
{    
    
if (Fa[x]==x) return x;    
    
else return Fa[x]=Find(Fa[x]);    
}
    
void LCA(int p)    
{    
    
int i, t;    
    map 
<intint> ::iterator itr;    
    Check[p]
=0;    
    
for (itr=Map[p].begin(); itr!=Map[p].end(); itr++)    
        
if (Check[itr->first]==-1)    
        
{    
            Path[itr
->first]=Path[p]+itr->second;    
            LCA(itr
->first); Fa[itr->first]=p;    
        }
    
        Check[p]
=1;    
        
for (i=0; i<Mark[p].size(); i++)    
        
{    
            t
=Mark[p][i];    
            
if (Q[t].x==p)   
            
{   
                
if (Check[Q[t].y]==1&&Q[t].val==0)   
                
{   
                    Cnt[Find(Q[t].y)]
++; Q[t].val=1;   
                }
   
            }
   
            
else if (Q[t].y==p)   
            
{   
                
if (Check[Q[t].x]==1&&Q[t].val==0)   
                
{   
                    Cnt[Find(Q[t].x)]
++; Q[t].val=1;   
                }
   
            }
   
        }
    
        
return;    
}
   

 

 二、 LCA 与RMQ :

// 资料:RMQ 与LCA 问题( 郭华阳).ppt

RMQ 向LCA 转化:设序列A 的长度为N

    1 、设序列中最小值为为Ak ,建立优先级为Ak 的根结点Tk ;

    2 、将A[1..k-1] 递归建树作为Tk 的左子树;

3 、将A[k+1..N] 递归建树作为Tk 的右子树。

RMQ(A, i, j) 查询A[i..j] 的最值即为LCA(T, i, j) 。

LCA 向RMQ 转化:

    对有根树T 进行DFS ,将遍历到的结点按照顺序记下,得到一个长度为2*N-1 的序列,称之为T 的欧拉序列F ,每个结点在欧拉序列中出现,记录pos[u] 为结点u 在欧拉序列中

第一次出现的位置。

    LCA(T, u, v)=RMQ(B, pos(u), pos(v))


RMQ与LCA算法
1、RMQ问题的ST算法

如果一个算法的预处理时间为f(n),查询时间为g(n),则我们称这个算法的总复杂度为<f(n),g(n)>。

Range Minimum Query(RMQ)

在一个数组上,求两个特定的序号之间的最小的数的位置的问题,就是RMQ问题。在查询的时候,最好返回所求结果在数组中的序号,而不是直接返回值的大小。

A[0]

A[1]

A[2]

A[3]

A[4]

A[5]

A[6]

A[7]

A[8]

A[9]

   10

   12

   6

   7

   9

   4

   66

100

   3

   0

      RMQ(0,9)=9,   RMQ(0,7)=5,   RMQ(2,9)=0

Sparse Table (ST) 算法:

一个很好的预处理的方式是记录所有长度为(2^i)的子序列的RMQ。用一个数组M[0,N-1][0,logN]保存预处理的结果,M[i][j]代表起点为i,长度为(2^j)的子序列的RMQ。那样,预处理每一个M[i][j]的时候就用动态规划来实现就可以了。

状态转移方程:

if ( A[M[i][j-1]]   <   A[M[i+2^(j-1)][j-1]] )

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

else

M[i][j] = M[i+2^(j-1)][j-1];

预处理的函数如下:

void preprocess()

{

for (i=0; i<N; i++)

M[i][0] = i;

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

for (i=0; i+(1<<j)-1<N; i++)

if (A[M[i][j-1]] < A[M[i+(1<<(j-1))][j-1]])

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

else

M[i][j]=M[i+(1<<(j-1))][j-1];

}

预处理完成后,要查询RMQ(i, j)。我们只需要找两个区间完全覆盖区间[i, j],那么两个区间的最小值中小的一个便是要求的结果。

则k=log(j-i+1),

if (A[M[i][k]] < A[M[j-2^k+1][k]])

RMQ(i ,j)=M[i][k];

else

RMQ(i, j)=M[j-2^k+1][k];

这个算法的总的复杂度是 <O(NlogN), O(1)>

2、 RMQ问题的线段树解法

线段树(区间数):

线段树是可以用于统计的一种非常优秀的数据结构,同样可以很好的解决RMQ问题。线段树又叫区间树,顾名思义,就是分区间来保存和查询所要处理的信息。

可以这样定义线段树:

struct node

{

    int left, right; // 区间的左边界和右边界

    int idx; // 这个区间内最小的值的序号

    node *l, *r; // 这个区间的左儿子和右儿子

};

node root; // 整个区间树的根节点     

然后初始化的函数:

void init(int left, int right, node *ptr)

{

    ptr->left=left, ptr->right=right;

    if (left==right)

    {

        ptr->l=NULL, ptr->r=NULL;

        ptr->idx=left;

        return;

    }

node *p=new node;

node *q=new node;

int mid=(left+right)/2;

init(left, mid, p);

init(mid+1, right, q);

ptr->l=p, ptr->r=q;

if (A[p->idx] < A[q->idx])

ptr->idx=p->idx;

else

ptr->idx=q->idx;

}

初始化的时候,调用语句:

init(0, N-1, &root);

查询RMQ(L, R)的函数:
int query(int L, int R, node *ptr)
{

if (L<=ptr->left && R>=ptr->right)

return ptr->idx;

int res;

int temp=0x7fffffff;

int mid=(ptr->left+ptr->right)/2;

i f (L<=mid)

{

int k=query(L, R, ptr->l);

temp=A[k];

res=k;

}

if (R>=mid+1)

{

int k=query(L, R, ptr->r);

if (A[k]<temp)

res=k;

}

return res;
}

查询的时候,调用语句:

int idx=query(L, R, &root);

用线段树解决RMQ问题的总复杂度是 <O(N), O(logN)>

用ST算法和用线段树解决RMQ问题各有自己的优先。只有查询的时候用ST算法和线段都可以解决,可以根据实际情况选择最优的算法。但是要修改数组中的值的时候,就只能用线段树来做了,ST算法不支持修改, 线段树的修改复杂度是O(logN)的


3、 LCA问题及其与RMQ问题间的转换

在一颗树上,两个节点p和q的最近公共祖先就是离根节点最远的p和q的祖先。下面介绍一种简单的算法( 类似于RMQ问题的ST算法的思想 )。

定义下列数组:

I nt T[N]; // T[i]代表节点i的父节点,根节点的父节点为-1

I nt L[N]; // L[i]代表节点i的高度,根节点的高度为1

I nt P[N][logN]; // P[i][j]代表节点i的第(1<<j)个祖先

运用动态规划的思想,状态转移方程如下:

if (j==0)

p[i][j]=T[i];

else

p[i][j]=p[P[i][j-1]][j-1];

预处理的函数如下:

void ptrprocess()

{

      for (i=0; i<N; i++)

      for (j=0; (1<<j)<N; j++)

      P[i][j]=-1;

      for (i=0; i<N; i++)

      P[i][0]=T[i];

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

      for (i=0; i<N; i++)

      if (P[i][j-1]!=-1)

      P[i][j]=P[P[i][j-1]][j-1];
}

查询LCA(p, q)的函数如下:

int query(int p, int q)

{

      // 不失一般性,让p节点的高度始终大于q节点的高度

      if (L[p]<L[q])

      swap(p, q);

      // 计算log(L[p])的值

      int log;

      for (log=0; (1<<log)<L[p]; log++) ;

      log--;

      // 计算节点p的与节点q在同一高度上的祖先

      for (i=log; i>=0; i--)

      if (L[p]-(1<<i) >= L[q])

      p=P[p][i];

      // 如果节点p的祖先是q,就返回节点p

      if (p==q)

      return p;

      // 节点p,q在同一高度上,计算LCA(p,q)

      for (i=log; i>=0; i--)

      if (P[p][i]!=-1 && P[p][i]!=P[q][i])

      {

            p=P[p][i];

            q=P[q][i]; 
            }

            return T[p];
}

这个算法的总复杂度是 <O(NlogN), O(logN)> 的。

LCA问题转化为RMQ问题:

先用深度优先搜索求出树的欧拉序列,然后建立一下信息:

int E[2*N]; // 树的欧拉序列(前序,中序,后序都可以)

int L[2*N]; // L[i]对应着E[i]的高度

int F[2*N]; // F[i]代表节点i第一次在欧拉序列中出现的位置

求节点p和q的的最近公共祖先。在欧拉序列中,节点p第一次出现的位置F[p],节点q第一次出现的位置F[q],位置F[p]和F[q]之间的高度最小的节点就应该是LCA(p, q),而这个高度最小的节点则可以通过对L序列求RMQ(F[p], F[q])得到,所以最后总结出 LCA(p,q)=E[RMQ(F[p],F[q])]

而将LCA问题转化为RMQ问题的复杂度是 O(N) 的。

推荐题目:

http://acm.pku.edu.cn/JudgeOnline/problem?id=2104

http://acm.pku.edu.cn/JudgeOnline/problem?id=2763

posted on 2010-05-08 12:11 superKiki 阅读(2629) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿

随笔档案

文章分类

我的好友

一些常去的网站

搜索

最新评论

阅读排行榜

评论排行榜