最近公共祖先-三(RMQ-ST)

描述

上上回说到,小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] << "
";
        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] << "
";
    }
    getchar();
    getchar();
    return 0;
}
原文地址:https://www.cnblogs.com/xyqxyq/p/10211382.html