#1067 : 最近公共祖先·二
时间限制:10000ms
单点时限:1000ms
内存限制:256MB
描述
上上回说到,小Hi和小Ho用非常拙劣——或者说粗糙的手段山寨出了一个神奇的网站,这个网站可以计算出某两个人的所有共同祖先中辈分最低的一个是谁。远在美国的他们利用了一些奇妙的技术获得了国内许多人的相关信息,并且搭建了一个小小的网站来应付来自四面八方的请求。
但正如我们所能想象到的……这样一个简单的算法并不能支撑住非常大的访问量,所以摆在小Hi和小Ho面前的无非两种选择:
其一是购买更为昂贵的服务器,通过提高计算机性能的方式来满足需求——但小Hi和小Ho并没有那么多的钱;其二则是改进他们的算法,通过提高计算机性能的利用率来满足需求——这个主意似乎听起来更加靠谱。
于是为了他们第一个在线产品的顺利运作,小Hi决定对小Ho进行紧急训练——好好的修改一番他们的算法。
而为了更好的向小Ho讲述这个问题,小Hi将这个问题抽象成了这个样子:假设现小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁?
- 样例输入
输入
每个测试点(输入文件)有且仅有一组测试数据。
每组测试数据的第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的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。
4 Adam Sam Sam Joey Sam Micheal Adam Kevin 3 Sam Sam Adam Sam Micheal Kevin
样例输出
Sam Adam Adam
分析:裸的LCA离线算法。留作模板。
转自:http://taop.marchtea.com/04.04.html
Tarjan算法 (以发现者Robert Tarjan命名)是一个在图中寻找强连通分量的算法。算法的基本思想为:任选一结点开始进行深度优先搜索dfs(若深度优先搜索结束后仍有未访问的结点,则再从中任选一点再次进行)。搜索过程中已访问的结点不再访问。搜索树的若干子树构成了图的强连通分量。
应用到咱们要解决的LCA问题上,则是:对于新搜索到的一个结点u,先创建由u构成的集合,再对u的每颗子树进行搜索,每搜索完一棵子树,这时候子树中所有的结点的最近公共祖先就是u了。
引用一个例子,如下图(不同颜色的结点相当于不同的集合):
假设遍历完10的孩子,要处理关于10的请求了,取根节点到当前正在遍历的节点的路径为关键路径,即1-3-8-10,集合的祖先便是关键路径上距离集合最近的点。
比如:
- 1,2,5,6为一个集合,祖先为1,集合中点和10的LCA为1
- 3,7为一个集合,祖先为3,集合中点和10的LCA为3
- 8,9,11为一个集合,祖先为8,集合中点和10的LCA为8
- 10,12为一个集合,祖先为10,集合中点和10的LCA为10
得出的结论便是:LCA(u,v)便是根至u的路径上到节点v最近的点。
但关键是 Tarjan算法是怎么想出来的呢?再给定下图,你是否能看出来:分别从结点1的左右子树当中,任取一个结点,设为u、v,这两个任意结点u、v的最近公共祖先都为1。
于此,我们可以得知:若两个结点u、v分别分布于某节点t 的左右子树,那么此节点 t即为u和v的最近公共祖先。更进一步,考虑到一个节点自己就是LCA的情况,得知:
- 若某结点t 是两结点u、v的祖先之一,且这两结点并不分布于该结点t 的一棵子树中,而是分别在结点t 的左子树、右子树中,那么该结点t 即为两结点u、v的最近公共祖先。
这个定理就是Tarjan算法的基础。
如果要求多个任意两个结点的最近公共祖先,则相当于是批量查询。即在很多组的询问的情况下,或许可以先确定一个LCA。例如是根节点1,然后再去检查所有询问,看是否满足刚才的定理,不满足就忽视,满足就赋值,全部弄完,再去假设2号节点是LCA,再去访问一遍。
可此方法需要判断一个结点是在左子树、还是右子树,或是都不在,都只能遍历一棵树,而多次遍历的代价实在是太大了,所以我们需要找到更好的方法。这就引出了下面要阐述的Tarjan算法,即每个结点只遍历一次,怎么做到的呢,请看下文讲解。
Tarjan算法流程为:
Procedure dfs(u);
begin
设置u号节点的祖先为u
若u的左子树不为空,dfs(u - 左子树);
若u的右子树不为空,dfs(u - 右子树);
访问每一条与u相关的询问u、v
-若v已经被访问过,则输出v当前的祖先t(t即u,v的LCA)
标记u为已经访问,将所有u的孩子包括u本身的祖先改为u的父亲
end
普通的dfs 不能直接解决LCA问题,故Tarjan算法的原理是dfs + 并查集,它每次把两个结点对的最近公共祖先的查询保存起来,然后dfs 更新一次。如此,利用并查集优越的时空复杂度,此算法的时间复杂度可以缩小至O(n+Q),其中,n为数据规模,Q为询问个数。
i) 访问1的左子树
STEP 1:从根结点1开始,开始访问结点1、2、3
STEP 2:2的左子树结点3访问完毕
STEP 3:开始访问2的右子树中的结点4、5、6
STEP 4:4的左子树中的结点5访问完毕
STEP 5:开始访问4的右子树的结点6
STEP 6:结点4的左、右子树均访问完毕,故4、5、6中任意两个结点的LCA均为4
STEP 7:2的左子树、右子树均访问完毕,故2、3、4、5、6任意两个结点的LCA均为2
如上所述:进行到此step7,当访问完结点2的左子树(3),和右子树(4、5、6)后,结点2、3、4、5、6这5个结点中,任意两个结点的最近公共祖先均为2。
ii) 访问1的右子树
STEP 8:1的左子树访问完毕,开始访问1的右子树
STEP 9:开始访问1的右子树中的结点7、8
STEP 10
STEP 11
STEP 12:1的右子树中的结点7、8访问完毕
当进行到此step12,访问完1的左子树(2、3、4、5、6),和右子树(7、8)后,结点2、3、4、5、6、7、8这7个结点中任意两个结点的最近公共祖先均为1。
STEP 13:1的左子树、右子树均访问完毕
通过上述例子,我们能看到,使用此Tarjan算法能解决咱们的LCA问题。
题目链接:http://hihocoder.com/problemset/problem/1067
代码清单:
#include<map> #include<set> #include<queue> #include<stack> #include<cmath> #include<cstdio> #include<string> #include<cstring> #include<iostream> #include<algorithm> using namespace std; typedef long long ll; const int maxn = 1e5 + 5; struct Edge{ int v,id; Edge(int v,int id){ this -> v = v; this -> id = id; } }; int n,m,id,root; string name1,name2; bool hasfa[maxn]; map<string,int>idx; string str[maxn]; vector<int>graph[maxn]; int father[maxn]; int color[maxn],ans[maxn]; vector<Edge>edge[maxn]; int Find(int x){return x!=father[x] ? father[x]=Find(father[x]) : father[x]; } void init(){ for(int i=0;i<maxn;i++){ graph[i].clear(); edge[i].clear(); father[i]=i; } memset(ans,0,sizeof(ans)); memset(color,0,sizeof(color)); memset(hasfa,false,sizeof(hasfa)); idx.clear(); id=0; } int get_idx(string name){ if(idx.count(name)) return idx[name]; idx[name]=++id; str[id]=name; return id; } void input(){ scanf("%d",&n); for(int i=0;i<n;i++){ cin>>name1>>name2; int idx1=get_idx(name1); int idx2=get_idx(name2); graph[idx1].push_back(idx2); hasfa[idx2]=true; } scanf("%d",&m); for(int i=1;i<=m;i++){ cin>>name1>>name2; int idx1=get_idx(name1); int idx2=get_idx(name2); edge[idx1].push_back(Edge(idx2,i)); edge[idx2].push_back(Edge(idx1,i)); } } void tarjan(int u){ color[u]=1; for(int i=0;i<edge[u].size();i++){ int ID=edge[u][i].id; if(ans[ID]) continue; int v=edge[u][i].v; if(color[v]==0) continue; if(color[v]==1) ans[ID]=v; if(color[v]==2) ans[ID]=Find(v); } for(int i=0;i<graph[u].size();i++){ int vv=graph[u][i]; tarjan(vv); color[vv]=2; father[vv]=u; } } void solve(){ for(int i=1;i<=n;i++) if(!hasfa[i]) root=i; tarjan(root); for(int i=1;i<=m;i++) cout<<str[ans[i]]<<endl; } int main(){ init(); input(); solve(); return 0; }
版权声明:本文为博主原创文章,未经博主允许不得转载。