描述

上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的“最近公共祖先”网站,但是很快这样一个离线算法就出现了问题:如果只有一个人提出了询问,那么小Hi和小Ho很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算。毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。

那么问题就来了,如果每次计算都只针对一个询问进行的话,那么这样的算法事实上还不如使用最开始的朴素算法呢!但是如果每次要等上很多人一起的话,因为说不准什么时候才能够凑够人——所以事实上有可能要等上很久很久才能够进行一次计算,实际上也是很慢的!

“那到底要怎么办呢?在等到10分钟,或者凑够一定数量的人两个条件满足一个时就进行运算?”小Ho想出了一个折衷的办法。

“哪有这么麻烦!别忘了和离线算法相对应的可是有一个叫做在线算法的东西呢!”小Hi笑道。

小Ho面临的问题还是和之前一样:假设现在小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁?

提示:最近公共祖先无非就是两点连通路径上高度最小的点嘛!×Close

提示:最近公共祖先无非就是两点连通路径上高度最小的点嘛!

“那你快教我啊!”小Ho耐不住性子。

“不要急,且听我缓缓道来,还记得很久之前我和你说过的最近公共祖先其实就是这两个点连通路径上的那个折点么(参见hiho一下第十一周树的直径)”小Hi问道。

“记得!”

“这个折点也就是这2点所连路径上深度最小的那个点了!那么这个问题其实和我们之前所提到的那个求区间最小值的是不是差不多(参见hiho一下第十六周——RMQ-ST算法),只不过一个是在数组上的区间,一个是在树上的区间?”小Hi问道。

“你非要这么说那我只能说是啦。。但是树和数组还是差了挺远的吧。”小Ho表示汗颜。

小Hi点了点头,随即道:“那就这么弄一下,我从树的根节点开始进行深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来。这样,是不是就把一棵树转换成了一个数组?而找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点?

小Ho显然是没有料到小Hi还有这一招,一上来也是感觉明显就不对嘛,毕竟好好的树怎么随便就弄成数组了不是,但是静下心来仔细想想:“从第一个点离开(返回它的父亲节点),到从第二个点离开(返回它的父亲节点)的这一段路程,的确经过的深度最小的点就是‘最近公共祖先’这一个点!”

看着小Ho露出了惊讶的神情,小Hi满意的点了点头,道:“这就是一个很好的将树转换成数组来进行某些特殊算法的方法!而且你仔细看看就会发现转换出的数组的长度其实就是边数的2倍而已,也是O(n)的级别呢~”

“原来是这样!那这次我只需要简单的套用之前写的算法,很简单嘛!”小Ho笑道。

“那是自然,你也不看看之前我们积累了一个月呢,现在你要是还磨磨蹭蹭的,回国怎么向河蟹先生交代!”

“嘿嘿嘿……”

Close

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第1行为一个整数N,意义如前文所述。

每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。

每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。

每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。

对于100%的数据,满足N<=10^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,且每个输入文件中第一个出现的名字所确定的人是其他所有人的公共祖先

输出

对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。

Sample Input

4
Adam Sam
Sam Joey
Sam Micheal
Adam Kevin
3
Sam Sam
Adam Sam
Micheal Kevin

Sample Output

Sam
Adam
Adam

题解

对于这个题,它的核心部分其实就是通过找到每一个区间的深度最小值,然后把它的编号(第几次出现)写入dp数组。

当在LCA函数里面调用query函数的时候,因为之前已经通过RMQ-ST求得了 所有已知的区间的深度最小值所对应的编号(第几次出现),所以query函数就可以找到dp数组里面存的深度最小值对应的编号(第几次出现,不再一一具体指出)。

既然已知该区间深度最小值对应的编号,然后就可以通过之前深搜时写下的编号数组f找到对应的点,该点通过map已经存下了字符串和整型数对应的关系,进而也就找到了解。

数组f里面存的是第几次出现了哪个点。

这就是这段代码的核心思想,至于RMQ-ST算法的思想,我的另外一篇博客里面有,读者可以自己去找一下,也可以参观其它的博客。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<string>
#include <cmath>
using namespace std;
const int N = 200005;
struct edge
{
int to,nxt,d;
edge(int t = 0,int n = 0,int d = 0):to(t),nxt(n),d(d){}
}E[N*2];
int n;
int head[N*2],tot,deg[N];
int cnt,vis[N],f[N*2],rk[N*2],pos[N*2],dis[N],dp[N*2][35];
//f存储节点编号,rk存储节点深度,pos记录结点第一次出现的位置
map<string,int> mp;
string mm[N];
void init()
{
memset(head,-1,sizeof(head));
for(int i = 0;i < N;i++)
vis[i] = dis[i] = 0;
tot = cnt = 0;
mp.clear();
}
void add_edge(int s,int t,int d)
{
E[tot] = edge(t,head[s],d);
head[s] = tot++;
}
void dfs(int u,int depth)
{
vis[u] = 1;
f[++cnt] = u;
pos[u] = cnt;
rk[cnt] = depth;
for(int i = head[u];~i;i = E[i].nxt)//访问所有子结点
{
int v = E[i].to;
if(!vis[v])
{
//dis[v] = dis[u] + w;
dfs(v,depth+1);
f[++cnt] = u;
rk[cnt] = depth;
}
}
}
void RMQ(int n)
{
for(int i = 1;i <= n;i++)
dp[i][0] = i;//初始化dp数组,让数组里面区间长度为1的最小值设置为它本身
for(int j = 1;(1<<j) <= n;j++)
{
for(int i = 1;i+(1<<j)-1 <= n;i++)
{
int a = dp[i][j-1],b = dp[i + (1<<(j-1))][j-1];
dp[i][j] = rk[a] < rk[b] ? a : b;
}
}
}
int query(int l,int r)
{
int k = (int)(log(r - l + 1.0) / log(2.0));
int a = dp[l][k],b = dp[r-(1<<k)+1][k];
return rk[a] < rk[b] ? a : b;
}
int LCA(int u,int v)
{
int x = pos[u],y = pos[v];
if(x > y) swap(x,y);
int t = query(x,y);//找到深度最小的结点编号
return f[t];
}
int main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); int n,m;
init();
cin >> n;
string u,v;
int num = 0;
for(int i = 1;i <= n;i++)
{
cin >> u >> v;
if(mp[u] == 0)
mp[u] = ++num,mm[num] = u;
if(mp[v] == 0)
mp[v] = ++num,mm[num] = v;
//cout << mp[u] << " " << mp[v] << "\n";
add_edge(mp[u],mp[v],0);
add_edge(mp[v],mp[u],0);
deg[mp[v]]++;
}
dfs(1,1);
RMQ(cnt);
cin >> m;
while(m--)
{
cin >> u >> v;
int lca = LCA(mp[u],mp[v]);
cout << mm[lca] << "\n";
}
getchar();
getchar();
return 0;
}

最近公共祖先-三(RMQ-ST)的更多相关文章

  1. hihoCoder week17 最近公共祖先·三 lca st表

    记录dfs序列,dfn[tot] 记录第tot次访问的节点 然后查两点在dfs序中出现的第一次 id[u] id[v] 然后  找 dep[k] = min( dep[i] ) {i 属于 [id[u ...

  2. hihocoder1069 最近公共祖先·三(tarjin算法)(并查集)

    #1069 : 最近公共祖先·三 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的“最近公共祖先”网站,但是 ...

  3. 求LCA最近公共祖先的在线ST算法_C++

    ST算法是求最近公共祖先的一种 在线 算法,基于RMQ算法,本代码用双链树存树 预处理的时间复杂度是 O(nlog2n)   查询时间是 O(1) 的 另附上离线算法 Tarjan 的链接: http ...

  4. 【hihoCoder第十七周】最近公共祖先·三

    之前就写的是离线算法.思路就是先序一遍树,记录层数,然后高效RMQ就好.ST和线段树都能过. 以后有时间将之前的在线算法补上. #include <bits/stdc++.h> using ...

  5. LCA(最近公共祖先)——dfs+ST 在线算法

    一.前人种树 博客:浅谈LCA的在线算法ST表 二.沙场练兵 题目:POJ 1330 Nearest Common Ancestors 题解博客:http://www.cnblogs.com/Miss ...

  6. hihoCoder#1069 最近公共祖先·三

    原题地址 根据提示用Spase Table做 将Tree先展成List,因为数组长度等于边数的2倍,树中边数等于节点数-1,所以List数组只要开2倍节点数大小即可 WA了几次,原来是查询的时候出现左 ...

  7. 最近公共祖先(LCA)的三种求解方法

    转载来自:https://blog.andrewei.info/2015/10/08/e6-9c-80-e8-bf-91-e5-85-ac-e5-85-b1-e7-a5-96-e5-85-88lca- ...

  8. 『图论』LCA 最近公共祖先

    概述篇 LCA (Least Common Ancestors) ,即最近公共祖先,是指这样的一个问题:在一棵有根树中,找出某两个节点 u 和 v 最近的公共祖先. LCA 可分为在线算法与离线算法 ...

  9. 二叉树系列 - 求两节点的最低公共祖先,例 剑指Offer 50

    前言 本篇是对二叉树系列中求最低公共祖先类题目的讨论. 题目 对于给定二叉树,输入两个树节点,求它们的最低公共祖先. 思考:这其实并不单单是一道题目,解题的过程中,要先弄清楚这棵二叉树有没有一些特殊的 ...

随机推荐

  1. SCUT - 249 - A piece of Cake - 组合数学

    https://scut.online/contest/25/I 由结论:d维物体切n刀分成的部分=sum(C(n,0)~C(n,d)),直接算就行了.

  2. CodeForces 13C【DP】

    题意: 给你n个数,每次只能让一个数+1,或者-1,目标是最终使这个序列构成一个非递减的序列: n是5e3,复杂度n^2内.值是1e9: 思路: 可以发现子结构是保证一个区间的非递减, 如果只是dp[ ...

  3. 【水水水】678A - Johny Likes Numbers

    #include<stdio.h> #include<iostream> #include<cstdio> #include<queue> #inclu ...

  4. Codeforces732E Sockets

    首先检测有木有和Computer匹配的Socket,如果有则将其匹配. 然后将所有没有匹配的Socket连上Adapter,再去检测有木有Computer与Socket匹配. 重复这个操作31次,所有 ...

  5. EasyUI创建选项卡并判断是否打开

    //创建选项卡:判断选项卡是否打开,如果以打开则定位到选项卡,否则创建 function addPanel(title) { var bol = $('#main_tabs').tabs('exist ...

  6. java 强大的反射机制

    这段时间,在对接一个开源的版本时,发现由于依赖的开源版本api老是随着版本的变化而变化,导致代码经常需要修改,异常痛苦. 终于,在一个风和日丽的下午(五月末的广州异常暴晒),楼主下定决心要修掉这个大篓 ...

  7. wordpress模板安装

    wordpress的模板安装方法是: 1.把下载好的模板的目录整体复制到wordpress\wp-content\themes下面,不需要单独复制哪个文件 2.到后台的"外观"中选 ...

  8. 使用ansible对远程主机上的ssh公钥进行批量分发

    使用ansible对远程主机上的ssh公钥进行批量分发或者是删除修改操作 ansible内置了一个authorized_key模块,这个模块很好用,我们使用这个模块可以对远程 主机上的ssh公钥进行批 ...

  9. 设置Linux环境变量的方法和区别_Ubuntu/CentOS

    设置 Linux 环境变量可以通过 export 实现,也可以通过修改几个文件来实现,有必要弄清楚这两种方法以及这几个文件的区别. 通过文件设置 Linux 环境变量 首先是设置全局环境变量,对所有用 ...

  10. JSP文件过大无法编译

    JSP文件过大无法编译: The code of method _jspService(HttpServletRequest, HttpServletResponse) is exceeding th ...