首先请看定义:

一、最近公共祖先(Least Common Ancestors)
对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。
这里给出一个LCA的例子:
例一
对于T=<V,E>
V={1,2,3,4,5}
E={(1,2),(1,3),(3,4),(3,5)}
则有:
LCA(T,5,2)=1
LCA(T,3,4)=3
LCA(T,4,5)=3

二、RMQ问题(Range Minimum Query)
RMQ问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j]里的最小值下标。这时一个RMQ问题的例子:
例二
对数列:5,8,1,3,6,4,9,5,7 有:
RMQ(2,4)=3
RMQ(6,9)=6

然后给出两种问题各自的算法和解析

. RMQ问题的ST算法

const int MAXN=100000+1;

const int MAXF=17;

const int INF=0x7FFFFFFF;

//可?以?断?言?ceiil(log(MAXN,2))==MAXF

inline int max(int a,int b){return a>b?a:b;}

inline int min(int a,int b){return a<b?a:b;}

class{

int dp_max[MAXN][MAXF+1];//dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?

int dp_min[MAXN][MAXF+1];

public:

void init(int* a,int n){

for(int i=0;i<n;i++){

dp_max[i][0]=a[i];

dp_min[i][0]=a[i];

}

for(int f=1,s=1;s<n;s=(1<<f++)){

for(int i=0;i+s<n;i++){

dp_max[i][f]=max(dp_max[i][f-1],dp_max[i+s][f-1]);

dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);

}

}

}

int query_max(int l,int r){

if(l>r)return -INF;

int d=r-l+1;

int f;

for(f=0;(1<<f)<=d;f++);

f--;

return max(dp_max[l][f],dp_max[r-(1<<f)+1][f]);

}

int query_min(int l,int r){

if(l>r)return -INF;

int d=r-l+1;

int f;

for(f=0;(1<<f)<=d;f++);

f--;

return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);

}

}RMQ;

来看一下ST算法是怎么实现的(以最小值为例)最小值只需将min换成max即可:

首先是预处理,用一个DP解决。设a[i]是要求区间最值的数列,f[i,j]表示从第i个数起连续2^j个数中的最小值。例如数列3 2 4 5 6 8 1 2 9 7 ,f[1,0]表示第1个数起,长度为2^0=1的最小值,其实就是3这个数。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……从这里可以看出f[i,0]其实就等于a[i]。这样,Dp的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。f[i,j]就是这两段的最小值中的最小值。于是我们得到了动规方程F[i,j]=minF[ij-1],F[i+2^(j-i)j-1].

接下来是得出最值,也许你想不到计算出f[i,j]有什么用处,一般毛想想计算max还是要O(logn),甚至O(n)。但有一个很好的办法,做到了O(1)。还是分开来。如在上例中我们要求区间[2,8]的最小值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最小值我们可以直接由f[2,2]和f[5,2]得到。扩展到一般情况,就是把区间[l,r]分成两个长度为2^n的区间(保证有f[i,j]对应)。直接给出表达式:

k:=trunc(l(r-l+1)/ln(2));

ans:=min(F[lk],F[r-2^k+1,k]);这样就计算了从i开始,长度为2^t次的区间和从r-2^i+1开始长度为2^t的区间的最小值(表达式比较烦琐,细节问题如加1减1需要仔细考虑

. LCA问题的Tarjan离线算法

int tree[10001][100],in[10001],p[10001];

int cas,s,t;

int n,Q1,Q2;

bool v[10001];

void Make_Set(int t)

{

p[t]=t;

}

int Find_Set(int t)

{

if(t!=p[t])

{

p[t]=Find_Set(p[t]);

}

return p[t];

}

void Union(int u,int v)

{

p[v]=u;

}

int LCA(int u)

{

Make_Set(u);

int i;

for(i=1;i<=tree[u][0];i++)

{

LCA(tree[u][i]);

Union(u,tree[u][i]);

}

v[u]=1;

if(u==Q1&&v[Q2])

{

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

}

else if(u==Q2&&v[Q1])

{

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

}

return 0;

}

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

最后讲解他们的转换关系

RMQ问题与LCA问题的关系紧密,可以相互转换,相应的求解算法也有异曲同工之妙。下面给出LCA问题向RMQ问题的转化方法。

对树进行深度优先遍历,每当“进入”或回溯到某个结点时,将这个结点的深度存入数组E最后一位。同时记录结点i在数组中第一次出现的位置(事实上就是进入结点i时记录的位置),记做R[i]。如果结点E[i]的深度记做D[i],易见,这时求LCA(T,u,v),就等价于求E[RMQ(D,R[u],R [v])],(R[u]<R[v])。例如,对于第一节的例一,求解步骤如下:

数列E[i]为:1,2,1,3,4,3,5,3,1

R[i]为:1,2,4,5,3

D[i]为:0,1,0,1,2,1,2,1,0

于是有:

LCA(T,5,2) = E[RMQ(D,R[2],R[5])] = E[RMQ(D,2,7)] = E[3] = 1

LCA(T,3,4) = E[RMQ(D,R[3],R[4])] = E[RMQ(D,4,5)] = E[4] = 3

LCA(T,4,5) = E[RMQ(D,R[4],R[5])] = E[RMQ(D,5,7)] = E[6] = 3

易知,转化后得到的数列长度为树的结点数的两倍加一,所以转化后的RMQ问题与LCA问题的规模同次。

再举一个例子帮助理解:

(1)

/ \

(2)   (7)

/ \     \

(3) (4)   (8)

/   \

(5)    (6)

一个nlogn 预处理,O(1)查询的算法.

Step 1:

按先序遍历整棵树,记下两个信息:结点访问顺序和结点深度.

如上图:

结点访问顺序是: 1 2 3 2 4 5 4 6 4 2 1 7 8 7 1 //共2n-1个值

结点对应深度是: 0 1 2 1 2 3 2 3 2 1 0 1 2 1 0

Step 2:

如果查询结点3与结点6的公共祖先,则考虑在访问顺序中

3第一次出现,到6第一次出现的子序列: 3 2 4 5 4 6.

这显然是由结点3到结点6的一条路径.

在这条路径中,深度最小的就是最近公共祖先(LCA). 即

结点2是3和6的LCA.

Step 3:

于是问题转化为, 给定一个数组R,及两个数字i,j,如何找出

数组R中从i位置到j位置的最小值..

如上例,就是R[]={0,1,2,1,2,3,2,3,2,1,0,1,2,1,0}.

i=2;j=7;

接下来就是经典的RMQ问题.

总结:

RMQ是给定一列数,动态询问[i,j]区间内的最小(或最大值)。

LCA是给定一棵树,动态询问u和v的最近公共祖先。

解决这两种问题都有个很重要的倍增思想(这个思想在后缀数组方面亦有所应用)。

关键需要记住的是

在LCA预处理的时候

p[i,j] 表示i的2^j 倍祖先

那么就有一个递推式子 p[i,j]=p[p[i,j-1],j-1]

RMQ和LCA可以相互转化。。   所以只要记住一种就行了。。

RMQ转LCA的时候是生成一棵类似于堆的递归树;LCA转RMQ的时候用到的是深度优先遍历。

主要掌握的不在于算法,而是在于倍增思想

附三份源代码

//POJ_3264 RMQ ST算?法?

#include <iostream>

using namespace std;

const int MAXN=100000+1;

const int MAXF=17;

const int INF=0x7FFFFFFF;

//可?以?断?言?ceiil(log(MAXN,2))==MAXF

inline int max(int a,int b){return a>b?a:b;}

inline int min(int a,int b){return a<b?a:b;}

class{

int dp_max[MAXN][MAXF+1];//dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?

int dp_min[MAXN][MAXF+1];

public:

void init(int* a,int n){

for(int i=0;i<n;i++){

dp_max[i][0]=a[i];

dp_min[i][0]=a[i];

}

for(int f=1,s=1;s<n;s=(1<<f++)){

for(int i=0;i+s<n;i++){

dp_max[i][f]=max(dp_max[i][f-1],dp_max[i+s][f-1]);

dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);

}

}

}

int query_max(int l,int r){

if(l>r)return -INF;

int d=r-l+1;

int f;

for(f=0;(1<<f)<=d;f++);

f--;

return max(dp_max[l][f],dp_max[r-(1<<f)+1][f]);

}

int query_min(int l,int r){

if(l>r)return -INF;

int d=r-l+1;

int f;

for(f=0;(1<<f)<=d;f++);

f--;

return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);

}

}RMQ;

int main()

{

int n,q;

int i,j,k;

int a[50005];

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

while (scanf("%d%d", &n, &q) !=EOF)

{

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

{

scanf("%d", &a[i]);

}

RMQ.init(a,n);

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

{

scanf("%d%d", &j, &k);

int t = RMQ.query_max(j-1,k-1);

int s = RMQ.query_min(j-1,k-1);

printf("%d\n",t-s);

}

}

return 0;

}

//POJ_ 1330 lCA转?RMQ

#include <iostream>

#include <vector>

using namespace std;

#define MAXN 20005

//LCA

int parent[MAXN];

vector<int> son[MAXN];

vector<int> E,D;

int R[MAXN*2];

const int MAXF=17;

const int INF=0x7FFFFFFF;

//可?以?断?言?ceiil(log(MAXN,2))==MAXF

inline int min(int a,int b){return a<b?a:b;}

class{

int dp_min[MAXN][MAXF+1]; //dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?

public:

void init(int n){

for(int i=0;i<n;i++){

dp_min[i][0]=D[i];

}

for(int f=1,s=1;s<n;s=(1<<f++)){

for(int i=0;i+s<n;i++){

dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);

}

}

}

int query_min(int l,int r){

if(l>r)return -INF;

int d=r-l+1;

int f;

for(f=0;(1<<f)<=d;f++);

f--;

return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);

}

int query_min_index(int l, int r)

{

int v = query_min(l,r);

if(v == -INF)return -INF;

int i;

for (i=l; i<=r; i++)

{

if (D[i] == v) return i;

}

}

}RMQ;

void DFS(int root, int deep)

{

int i;

E.push_back(root);

R[root] = E.size()-1;

D.push_back(deep);

for (i=0; i<son[root].size(); i++)

{

DFS(son[root][i],deep+1);

E.push_back(root);

D.push_back(deep);

}

}

int LCA(int l, int r)

{

RMQ.init(D.size());

int x = R[l];

int y = R[r];

if (x>y)

{

int temp = x;

x = y;

y= temp;

}

return E[RMQ.query_min_index(x,y)];

}

int main()

{

freopen("input.txt", "r", stdin);

freopen("output.txt", "w", stdout);

int N;

int n;

int i,j,k;

int a,b;

scanf("%d",&N);

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

{

scanf("%d", &n);

for (j=0; j<=n; j++)

{

parent[j] = j;

son[j].clear();

}

E.clear();

D.clear();

for (j=0; j<n-1; j++)

{

scanf("%d%d", &a,&b);

a--;

b--;

parent[b] = a;

son[a].push_back(b);

}

int root = 0;

while (parent[root] != root) root = parent[root];

DFS(root,0);

scanf("%d%d", &a,&b);

a--;

b--;

printf("%d\n",LCA(a,b)+1);

}

return 0;

}

//POJ_1330 LCA问?题?的?Tarjan离?线?算?法?

#include<iostream>

using namespace std;

int tree[10001][100],in[10001],p[10001];

int cas,s,t;

int n,Q1,Q2;

bool v[10001];

void Make_Set(int t)

{

p[t]=t;

}

int Find_Set(int t)

{

if(t!=p[t])

{

p[t]=Find_Set(p[t]);

}

return p[t];

}

void Union(int u,int v)

{

p[v]=u;

}

int LCA(int u)

{

Make_Set(u);

int i;

for(i=1;i<=tree[u][0];i++)

{

LCA(tree[u][i]);

Union(u,tree[u][i]);

}

v[u]=1;

if(u==Q1&&v[Q2])

{

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

}

else if(u==Q2&&v[Q1])

{

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

}

return 0;

}

int main()

{

scanf("%d",&cas);

while(cas--)

{

int i;

memset(tree,0,sizeof(tree));

memset(in,0,sizeof(in));

memset(v,0,sizeof(v));

scanf("%d",&n);

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

{

scanf("%d%d",&s,&t);

tree[s][++tree[s][0]]=t;

in[t]++;

}

scanf("%d%d",&Q1,&Q2);

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

{

if(in[i]==0)

break;

}

LCA(i);

}

system("pause");

return 0;

}

转载于:https://www.cnblogs.com/ACAC/archive/2010/05/24/1743139.html

看到的一个很不错的分析LCA和RMQ的文章(转载,先收着)相关推荐

  1. php上下翻页,一个很不错的PHP翻页类

    一个很不错的PHP翻页类,包含了使用例子,感谢Alan /* * Created on 2007-6-8 * Programmer : Alan , Msn - haowubai@hotmail.co ...

  2. 一个很不错的bash脚本编写教程

    一个很不错的bash脚本编写教程 建立一个脚本 Linux中有好多中不同的shell,但是通常我们使用bash (bourne again shell) 进行shell编程,因为bash是免费的并且很 ...

  3. css搜索的文本框,一个很不错的CSS改写的大表单文本框和搜索按钮组

    <一个很不错的CSS改写的大表单文本框和搜索按钮组>要点: 本文介绍了一个很不错的CSS改写的大表单文本框和搜索按钮组,希望对您有用.如果有疑问,可以联系我们. 先看效果图: 开始爬取 C ...

  4. 一个很不错的C++类的练习题目

    //一个很不错的C++类的练习题目1.定义一个C++枚举(作为枚举结构),有以下四个值:一次.每天.每周.每月. 为这个枚举定义一个重载的输出操作符<<,根据枚举值,它将每日.每周.每月和 ...

  5. yt88加密狗不识别_YT88 是天域一个很不错的加密狗工具,可以源码 ,还 外壳 ,带DLK开发包。 Windows Develop 256万源代码下载- www.pudn.com...

    文件名称: YT88下载  收藏√  [ 5  4  3  2  1 ] 开发工具: Windows_Unix 文件大小: 12909 KB 上传时间: 2016-05-01 下载次数: 0 提 供 ...

  6. 兄弟连,一个很不错的名字。

    兄弟连,一个很不错的名字. 不知道大家有没有看过"兄弟连"这部电视剧 . 一群来自五湖四海的年轻人聚在一起,他们都是一群普普通通的老百姓,但是却 在后来的训练下变成了一支特种部队. ...

  7. 一个很不错的支持Ext JS 4的上传按钮

    以前经常使用的swfUpload,自从2010年开始到现在,很久没更新了.而这几年,flash版本已经换了好多个,所以决定抛弃swfupload,使用新找到的上传按钮. 新的上传按钮由harrydel ...

  8. 推荐一个很不错的刷题网站

    分享 小伙伴们,还在为学习编程语言找不到好的练习题而苦恼吗?今天小菜分享一个刷题网站:https://www.codewars.com/dashboard.这个网站和力扣不同的是,这真的是适合人类的网 ...

  9. 一个很不错的开发管理中文个人网站

    很不错的开发管理内容. http://www.worldhello.net/ 转载于:https://www.cnblogs.com/chenge/archive/2005/02/27/110018. ...

最新文章

  1. Rad Studio IDE 代码编辑器增强工具 RADSplit
  2. 什么是死锁?死锁产生的四个必要条件?如何避免与预防死锁?
  3. Oracle-HWM(High Water Mark) 高水位解读
  4. Linux运维常见的硬件问题
  5. mysql update 几万 非常慢_Mysql优化专题
  6. 《数字图像处理 第三版》(冈萨雷斯)——第四章 频率域处理
  7. android studio trace,天猫Android性能优化1—AndroidStudio内置的Traceview视图
  8. sqlplus命令支持上、下翻功能
  9. 后台管理系统 - 权限设计
  10. 银行合规程序KYC、CDD、AML和TM
  11. excel表格数据库表字段带下划线转驼峰
  12. 刚换了Mac本这些快捷键你知道吗?
  13. NB-IOT的基础知识
  14. 史考特 容易忽略的开户细节
  15. 【Linux】物理CPU、CPU核数、逻辑CPU、超线程
  16. SQLSERVER中的 CEILING函数和 FLOOR函数
  17. 恶意软件检测技术综述
  18. Word设置表格文字上下居中
  19. Xmind8 Pro破解版
  20. ps:Netty服务端主动关闭问题

热门文章

  1. Linux下的SecureCRT破解方法
  2. 框架通讯契约——接口
  3. 软件设计师09-面向对象-图集
  4. powershell自动化操作AD域、Exchange邮箱系列(3)—重要的模块/API介绍Get-Aduser Get-Mailbox
  5. python_Pandas机器学习数据预处理
  6. 多重共线性问题的几种解决方法
  7. java对象模型 指令_JVM-Java内存模型-20200217(示例代码)
  8. mysql存储过程与触发器的例题
  9. 放在请求头目的_浅谈http的几种请求方法
  10. python定时器 是线程吗_定时器中断线程python