描述

上上回说到,小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

非常喜欢RMQ(ST)这个求区间最值的算法。

#include <iostream>
#include <cstring>
#include <string>
#include <algorithm>
#include <stack>
#include <vector>
#include <map>
#include <queue>
#include <set>
#include <cmath>
#include <cstdio>
#define inf 0x3f3f3f3f
#define ll long long
#define maxx 110000
using namespace std;
int n,m;
int pos[maxx],dep[maxx],f[maxx],dp[maxx][100],rt,tot;
//f表示经过的序号,dep深度 pos 表示每个点在f中第一次出现的位置
map<string,int > mp;
vector<int >vec[maxx];
string name[maxx];
//预处理 f,dep,pos数组
void dfs(int idx, int depth) // 预处理f,dep,pos数组
{
f[++tot] = idx;
dep[tot] = depth; //标记深度
pos[idx] = tot; //f,pos,dep数组作用看上面的例1。

for(int i = 0; i < vec[idx].size(); ++i)
{
dfs(vec[idx][i], depth+1);
f[++tot] = idx;
dep[tot] = depth;
}
}

void RMQ()
{
for(int i = 1; i <= tot; ++i)
dp[i][0] = i;
for(int j = 1; (1<<j) <= tot; ++j)
{
for(int i = 1; i+(1<<j)-1 <= tot; ++i)
{
int mid = i + (1 << (j-1)); //将区间分成长度相同的两段,mid为右区间第一个
if(dep[dp[i][j-1]] < dep[dp[mid][j-1]])
dp[i][j] = dp[i][j-1];
else
dp[i][j] = dp[mid][j-1];
}
}
}

int LCA(int l, int r)
{
l = pos[l];
r = pos[r];
if(l > r)
swap(l,r);
int len = r-l+1;
int len2 = log2(len);
if(dep[dp[l][len2]] < dep[dp[r-(1<<len2)+1][len2]])
return dp[l][len2];
return dp[r-(1<<len2)+1][len2];
}

int main()
{
cin >> n;
string a, b; //a父 b子

//建树
for(int i = 0; i < n; ++i)
{
cin >> a >> b;
// map容器mp 和 name数组 就是存一下名字字符串及序号,不必在乎顺序
if(!mp[a]) // !mp[a] 即 mp[a]为空,下面同理
{
mp[a] = ++tot;
name[mp[a]] = a;
}
if(!mp[b])
{
mp[b] = ++tot;
name[mp[b]] = b;
}
vec[mp[a]].push_back(mp[b]); // 向父节点后添加子节点
if(!i)
rt = mp[a]; //题中说了:每个输入文件中第一个出现的名字所确定的人是其他所有人的公共祖先。
}

//dfs转换一维数组
tot = 0;
dfs(rt, 0);

RMQ(); //rmq预处理

//LCA查找
cin >> m;
while(m--)
{
cin >> a >> b;
int aa = mp[a], bb = mp[b];
int ans = LCA(aa,bb); // ans 为 aa 到 bb 的辈分最小共同祖先的序号
cout << name[f[ans]] << endl;
}
return 0;
}