最近公共祖先 朴素 离线 在线 算法合集
来源:互联网 发布:卫计委数据平台 编辑:程序博客网 时间:2024/05/07 21:04
描述
小Ho最近发现了一个神奇的网站!虽然还不够像58同城那样神奇,但这个网站仍然让小Ho乐在其中,但这是为什么呢?
“为什么呢?”小Hi如是问道,在他的观察中小Ho已经沉迷这个网站一周之久了,甚至连他心爱的树玩具都弃置一边。
“嘿嘿,小Hi,你快过来看!”小Ho招呼道。
“你看,在这个对话框里输入我的名字,在另一个对话框里,输入你的名字,再点这个查询按钮,就可以查出来……什么!我们居然有同一个祖祖祖祖祖爷爷?”
“诶,真是诶……这个网站有点厉害啊。”小Hi不由感叹道。
“是啊,这是什么算法啊,这么厉害!”小Ho也附和道。
“别2,我说的是他能弄到这些数据很厉害,而人类的繁殖树这种层数比较浅的树对这类算法的要求可是简单的不得了,你都能写出来呢!”小Hi道。
“啊?我也能写出来?可是……该从哪开始呢?”小Ho困惑了。
小Ho要面临的问题是这样的,假设现在他知道了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^2,M<=10^2, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人)。
输出
对于每组测试数据,对于每个小Hi的询问,输出一行,表示查询的结果:如果根据已知信息,可以判定询问中的两个人存在共同的祖先,则输出他们的所有共同祖先中辈分最低的一个人的名字,否则输出-1。
11JiaYan JiaDaihuaJiaDaihua JiaFuJiaDaihua JiaJingJiaJing JiaZhenJiaZhen JiaRongJiaYuan JiaDaishanJiaDaishan JiaSheJiaDaishan JiaZhengJiaShe JiaLianJiaZheng JiaZhuJiaZheng JiaBaoyu3JiaBaoyu JiaLianJiaBaoyu JiaZhengJiaBaoyu LinDaiyu
JiaDaishanJiaZheng-1
1、朴素算法
提示:不着急,慢慢来,另外我有一个问题:挖掘机技术哪家强?!
小Hi道:“这个问题也应该是树结构许许多多问题中颇为经典的一个了,如果就简单的将父子关系视作树结构中的父子结点关系的话,这个问题其实就是在问树中两个结点层数最高的公共祖先——也就是所谓的最近公共祖先,而这个问题有非常多的解决方法,分别适合于不同的场景,像在线算法离线算法什么的现在说给你听你也不一定能够很快理解。”
“但是……你不是说我能够写出来的么?”小Ho纳闷了。
“所以说我们慢慢来不要急,先教你个最简单的法子。”小Hi如是说道。
“最简单的法子……如果数据量不大的话,我完全可以直接将这两个人的祖先全部找出来,然后取它们的交集,然后再找到其中辈分最低的一个不就行了。”小Ho思考了会这般说道。
小Hi点了点头道:“差不多就是这样!当然你也先将一个人的祖先全都标记出来,然后顺着另一个的父亲一直向上找,直到找到第一个被标记过的结点,便是它们的最近公共祖先结点了。”
“原来这么简单!”小Ho笑道:“那我先去写着了。”
#pragma comment(linker, "/STACK:102400000,102400000")#include <iostream>#include <vector>#include <string>#include <algorithm>#include <cstdio>#include <cstring>#include <ctime>#include <cstdlib>#include <set>#include <map>#include <cctype>#include <list>#include <cmath>#include <bitset>#include <queue>#include <stack>#include <sstream>#include <functional>#include <cassert>using namespace std;int RL(char _str[], int _maxlen = 1e8) {if (fgets(_str, _maxlen, stdin) == NULL) return -1;int i = 0;while (_str[i]) ++i;if (_str[i - 1] == '\n') _str[--i] = 0;return i;}inline int RI() {int temp;scanf("%d", &temp);return temp;}inline long long RLL() {long long temp;scanf("%I64d", &temp);return temp;}inline double RD() {double temp;scanf("%lf", &temp);return temp;}template <class T1, class T2>inline void Max(T1 &a, T2 b) { if (b > a) a = b; }template <class T1, class T2>inline void Min(T1 &a, T2 b) { if (b < a) a = b; }typedef pair<int, int> pii;typedef pair<double, double> pdd;typedef long long LL;const int inf = 0x3f3f3f3f;const LL INF = 0x3f3f3f3f3f3f3f3f;const double eps = 1e-8;const int mod = 100007;const int maxn = 100 + 9;int N, M;int main() {//std::ios::sync_with_stdio(0);//std::cin.tie(0);#ifdef NIGHT_13freopen("in.txt", "r", stdin);//freopen("myout.txt", "w", stdout);#endifint CAS = 0;int id = 1;map<string, string> par;scanf("%d", &N);while (N--) {char p1[100], p2[100];scanf("%s%s", p1, p2);par[p2] = p1;}scanf("%d", &M);while (M--) {char p1[100], p2[100];scanf("%s%s", p1, p2);string a(p1), b(p2);set<string> fa;fa.insert(a);while (par.find(a) != par.end()) {fa.insert(par[a]);a = par[a];}while (fa.find(b) == fa.end() && par.find(b) != par.end()) {b = par[b];}if (fa.find(b) != fa.end()) puts(b.c_str());else puts("-1");}return 0;}
2、离线算法(tarjan)
提示一:老老实实分情况讨论就不会出错的啦!
“唔……我感觉你这个问题和上次提出的问题没有什么太大区别嘛,还简化了许多……那我就用上次的算法不行么?”小Ho疑问道。
“NoNoNo,仔细注意一下数据范围,这可不是上次的算法就能够解决的~”小Hi道。
“是诶,上次无论是人数还是询问数,都远远少于这一次的……那么我该如何进行计算呢?”小Ho不禁问道。
“我认为有这样两种法子,其中一种呢,就是将一段时间内的询问收集起来统一处理,利用一些与询问个数无关的算法来进行计算,这样时间复杂度在平摊到每个询问的时候就不会很高了。”小Hi缓缓道来。
“听起来很有道理。”
“就是很有道理好么!”小Hi道:“我们不妨再抽象这个问题,其实本质就是给你一棵树,然后每次询问是求这棵树种某两个结点的最近公共祖先——所有公共祖先中层数最深的。”
“是的!而且这个最近公共祖先怎么和之前那一次所说的转折点很类似啊?”小Ho似乎回忆起了什么往事。
“差不多一个意思把,重点是在于我接下来要说的——用于求解最近公共祖先问题的离线算法,为什么要叫做离线算法呢,因为我需要一次性收集若干个询问之后才能通过这个算法一同计算出这些询问的答案,而不能像一个在线系统一样对于每一个询问都即时的进行计算并给出答复。”小Hi解释道。
“你说了这么多,可我还是不知道这个算法是怎么回事呢!”小Ho抱怨道。
“别急,你看这个图——好吧,其实是棵树,代表着我们这个问题的一个输入,而这几个是我们要处理的询问。”小Hi在准备好的黑板上写写画画道。
“好的。”小Ho点点头。
“而我现在要做的呢,是以深度优先搜索的顺序来访问这棵树。”小Hi继续道:“在这个过程中,我会给这棵树的结点染色,一开始所有结点都是白色的。而当我第一次经过某个结点的时候,我会将它染成灰色,而当我第二次经过这个结点的时候——也就是离开这棵子树的时候,我会将它染成黑色。”
“但是这样做的意义何在呢?”小Ho的问题来了。
“举个例子,当我们深度优先搜索到A结点时,我们发现A结点和B结点是我们需要处理的一组询问。”小Hi在黑板上画着。
小Ho点了点头。
“这个时候,这个图的染色情况是这样的。”小Hi顺手就涂上了颜色,继续道:“我们这个时候就要去查看B结点的颜色——灰色,说明我们进入了以B结点为根的子树,但是还没有从这棵子树中出去,你知道这意味着什么吗?”
“意味着A结点在B结点所在的子树中——那么B结点就是A和B结点的最近公共祖先了?”小Ho答道。
“没错,但这只是一种简单的情况,如果我询问的不是A和B这两个结点,而是A和C这两个结点,那么在访问到A的时候,C的颜色是黑色的,这时候我该怎么处理呢?”小Hi继续问道。
“唔……首先肯定只能从所有灰色结点中找——这是A结点的所有祖先结点!那么……就是C结点向上的第一个灰色结点?”小Ho答道。
“是的,但是你这样做不会和上一次的朴素做法一样低效率么?”小Hi反问道。
“的确是的……每次都要向上找会很吃不消的。”小Ho困惑了:“那么我该怎么办呢?”
“先别着急,我们把之前的分情况讨论结束,再来细谈这个问题……那么接下来只有一种可能了,如果询问的时A和P这两个结点,而此时P还是白色的,你觉得怎么处理比较合适?”小Hi道。
“唔……我觉得把,既然这个时候P还是白色,那么就先不要管这个询问了,毕竟现在关于P的信息一点都不知道。而且反正深度优先搜索会处理到P结点,那个时候A结点肯定已经不是白色了,就可以沿用之前的方法进行解决了!”小Ho沉思了一会,答道。
“是的!那么你有没有发现,这样一遍处理下来,你就可以求出所有准备好的询问的结果了——我先计算每个结点涉及到的询问,然后在深度优先搜索的过程中对结点染色,如果发现当前访问的结点是涉及到某个询问,那么我就看这个询问中另一个结点的颜色,如果是白色,则留待之后处理,如果是灰色,那么最近公共祖先必然就是这个灰色结点,如果是黑色,那么最近公共祖先就是这个黑色结点向上的第一个灰色结点。”小Hi总结道:“而我们唯一剩下的问题,就是怎么样快速的找到一个黑色结点向上的第一个灰色结点。”
小Ho若有所思的点了点头。
提示二:并查集其实长得很像一棵树你们不觉得么?
“还记得上周我和你讲的并查集问题么?”小Hi问道。
“还记得呢!但是那和这个问题又有什么关系呢?”小Ho问道。
“我们来这样想,当深度优先搜索进行到刚刚第二次经过C结点这一步的时候,是不是以C为根的子树中所有结点,它们向上找的第一个灰色结点都是C结点?”小Hi没有直接回答小Ho的问题,反倒提出了一个新的问题。
“是的。”
“那么如果我们将C这棵子树视为一个集合,而将C结点视为这个集合的代表元素的话,那么我在第二次经过C结点之后,将C结点染成黑色后,以C为根的子树中的所有结点的代表元素——或者说向上找的第一个灰色结点都变成了C的父亲结点——D结点?那么这一过程,是不是其实就是将C结点代表的集合合并到了D结点代表的集合中去了?”小Hi问道。
“唔……还真是,每个结点最开始都是一个独立的集合,每当一个结点由灰转黑的时候,就将它所在的集合合并到其父亲结点所在的集合中去。这样无论什么时候,任意一个黑色结点所在集合的代表元素就是这个结点向上的第一个灰色结点!也就是说,我只要在深度优先搜索的过程中维护这样的一些集合,我就能够对于每一个黑色结点,非常快捷的求出在当前的染色状况下,它向上的第一个灰色结点。”小Ho惊道:“这么一来,之前遗留的问题就得到了解决。”
“是的呢!而之所以你可以这样做的原因,这是因为这样的染色是不可逆的,对于一个黑色结点来说,它向上找的第一个灰色结点只会越来越高——这和集合的性质是相似的!”
“原来是这样!”小Ho感慨道。
“所以你接下来要做的事情,就是在深度优先搜索的过程中,不断的给结点染色,同时维护好这样的一些集合,并且对于询问一一做出回答呢!”小Hi笑道:“这可是有点难度哦~”
“没事!这对我来说,可是小菜一碟呢!”
#pragma comment(linker, "/STACK:102400000,102400000")#include <iostream>#include <vector>#include <string>#include <algorithm>#include <cstdio>#include <cstring>#include <ctime>#include <cstdlib>#include <set>#include <map>#include <cctype>#include <list>#include <cmath>#include <bitset>#include <queue>#include <stack>#include <sstream>#include <functional>#include <cassert>using namespace std;int RL(char _str[], int _maxlen = 1e8) {if (fgets(_str, _maxlen, stdin) == NULL) return -1;int i = 0;while (_str[i]) ++i;if (_str[i - 1] == '\n') _str[--i] = 0;return i;}inline int RI() {int temp;scanf("%d", &temp);return temp;}inline long long RLL() {long long temp;scanf("%I64d", &temp);return temp;}inline double RD() {double temp;scanf("%lf", &temp);return temp;}template <class T1, class T2>inline void Max(T1 &a, T2 b) { if (b > a) a = b; }template <class T1, class T2>inline void Min(T1 &a, T2 b) { if (b < a) a = b; }typedef pair<int, int> pii;typedef pair<double, double> pdd;typedef long long LL;const int inf = 0x3f3f3f3f;const LL INF = 0x3f3f3f3f3f3f3f3f;const double eps = 1e-8;const int mod = 100007;const int maxn = 1e5 + 9;int N, M, par[maxn];map<string, int> id;string name[maxn];vector<int> G[maxn];vector<pii> Q[maxn];int lca[maxn];int Find(int x) {if (par[x] == x) return x;return par[x] = Find(par[x]);}void LCA_Tarjan(int x, int p) {par[x] = x;for (int i = 0; i < G[x].size(); ++i) {if (G[x][i] != p) LCA_Tarjan(G[x][i], x);}for (int i = 0; i < Q[x].size(); ++i) {if (par[Q[x][i].first]) lca[Q[x][i].second] = Find(Q[x][i].first);}par[x] = p;}int main() {//std::ios::sync_with_stdio(0);//std::cin.tie(0);#ifdef NIGHT_13freopen("in.txt", "r", stdin);//freopen("myout.txt", "w", stdout);#endifint CAS = 0;int _id = 0;scanf("%d", &N);while (N--) {char p1[100], p2[100];scanf("%s%s", p1, p2);int ida, idb;if (!(ida = id[p1])) { ida = id[p1] = ++_id; name[_id] = p1; }if (!(idb = id[p2])) { idb = id[p2] = ++_id; name[_id] = p2; }G[ida].push_back(idb);G[idb].push_back(ida);}scanf("%d", &M);for (int i = 1; i <= M; ++i) {char p1[100], p2[100];scanf("%s%s", p1, p2);int a = id[p1], b = id[p2];Q[a].push_back(pii(b, i));Q[b].push_back(pii(a, i));}LCA_Tarjan(1, 0);for (int i = 1; i <= M; ++i) {puts(name[lca[i]].c_str());}return 0;}
3、在线算法(RMQ-ST)
提示:最近公共祖先无非就是两点连通路径上高度最小的点嘛!
“那你快教我啊!”小Ho耐不住性子。
“不要急,且听我缓缓道来,还记得很久之前我和你说过的最近公共祖先其实就是这两个点连通路径上的那个折点么(参见hiho一下第十一周树的直径)”小Hi问道。
“记得!”
“这个折点也就是这2点所连路径上深度最小的那个点了!那么这个问题其实和我们之前所提到的那个求区间最小值的是不是差不多(参见hiho一下第十六周——RMQ-ST算法),只不过一个是在数组上的区间,一个是在树上的区间?”小Hi问道。
“你非要这么说那我只能说是啦。。但是树和数组还是差了挺远的吧。”小Ho表示汗颜。
小Hi点了点头,随即道:“那就这么弄一下,我从树的根节点开始进行深度优先搜索,每次经过某一个点——无论是从它的父亲节点进入这个点,还是从它的儿子节点返回这个点,都按顺序记录下来。这样,是不是就把一棵树转换成了一个数组?而找到树上两个节点的最近公共祖先,无非就是找到这两个节点最后一次出现在数组中的位置所囊括的一段区间中深度最小的那个点?”
小Ho显然是没有料到小Hi还有这一招,一上来也是感觉明显就不对嘛,毕竟好好的树怎么随便就弄成数组了不是,但是静下心来仔细想想:“从第一个点离开(返回它的父亲节点),到从第二个点离开(返回它的父亲节点)的这一段路程,的确经过的深度最小的点就是‘最近公共祖先’这一个点!”
看着小Ho露出了惊讶的神情,小Hi满意的点了点头,道:“这就是一个很好的将树转换成数组来进行某些特殊算法的方法!而且你仔细看看就会发现转换出的数组的长度其实就是边数的2倍而已,也是O(n)的级别呢~”
“原来是这样!那这次我只需要简单的套用之前写的算法,很简单嘛!”小Ho笑道。
“那是自然,你也不看看之前我们积累了一个月呢,现在你要是还磨磨蹭蹭的,回国怎么向河蟹先生交代!”
“嘿嘿嘿……”
#pragma comment(linker, "/STACK:102400000,102400000")#include <iostream>#include <vector>#include <string>#include <algorithm>#include <cstdio>#include <cstring>#include <ctime>#include <cstdlib>#include <set>#include <map>#include <cctype>#include <list>#include <cmath>#include <bitset>#include <queue>#include <stack>#include <sstream>#include <functional>#include <cassert>using namespace std;int RL(char _str[], int _maxlen = 1e8) {if (fgets(_str, _maxlen, stdin) == NULL) return -1;int i = 0;while (_str[i]) ++i;if (_str[i - 1] == '\n') _str[--i] = 0;return i;}inline int RI() {int temp;scanf("%d", &temp);return temp;}inline long long RLL() {long long temp;scanf("%I64d", &temp);return temp;}inline double RD() {double temp;scanf("%lf", &temp);return temp;}template <class T1, class T2>inline void Max(T1 &a, T2 b) { if (b > a) a = b; }template <class T1, class T2>inline void Min(T1 &a, T2 b) { if (b < a) a = b; }typedef pair<int, int> pii;typedef pair<double, double> pdd;typedef long long LL;const int inf = 0x3f3f3f3f;const LL INF = 0x3f3f3f3f3f3f3f3f;const double eps = 1e-8;const int mod = 100007;const int maxn = 1e5 + 9;int N, M, a[maxn*3], dp[20][maxn*3];map<string, int> id;string name[maxn];vector<int> G[maxn];int ix;void ToArr(int x, int p) {a[ix++] = x;for (int i = 0; i < G[x].size(); ++i) {if (G[x][i] != p) {ToArr(G[x][i], x);a[ix++] = x;}}}void InitRMQ() {ToArr(1, 0);for (int i = 0; i < ix; ++i) dp[0][i] = a[i];for (int i = 1; 1 << i <= ix; ++i) {for (int j = 0; j + (1 << i) <= ix; ++j) {dp[i][j] = min(dp[i - 1][j], dp[i - 1][j + (1 << (i - 1))]);}}}int RMQ(int l, int r) {if (l > r) swap(l, r);int k = log(r - l + 1) / log(2);return min(dp[k][l], dp[k][r - (1 << k) + 1]);}int main() {//std::ios::sync_with_stdio(0);//std::cin.tie(0);#ifdef NIGHT_13freopen("in.txt", "r", stdin);//freopen("myout.txt", "w", stdout);#endifint CAS = 0;int _id = 0;scanf("%d", &N);while (N--) {char p1[100], p2[100];scanf("%s%s", p1, p2);int ida, idb;if (!(ida = id[p1])) { ida = id[p1] = ++_id; name[_id] = p1; }if (!(idb = id[p2])) { idb = id[p2] = ++_id; name[_id] = p2; }G[ida].push_back(idb);G[idb].push_back(ida);}InitRMQ();scanf("%d", &M);for (int i = 1; i <= M; ++i) {char p1[100], p2[100];scanf("%s%s", p1, p2);int pa = id[p1], pb = id[p2];int l = ix, r = ix;while (a[l] != pa) --l;while (a[r] != pb) --r;printf("%s\n", name[RMQ(l,r)].c_str());}return 0;}
- 最近公共祖先 朴素 离线 在线 算法合集
- 算法基础 - 最近公共祖先(在线算法/离线算法)
- LCA最近公共祖先 在线算法和离线算法 模板
- 最近公共祖先(LCA):离线&在线算法
- 最近公共祖先离线加在线倍增
- LCA最近公共祖先(tarjan离线算法)
- 最近公共祖先LCA离线算法
- LCA最近公共祖先的离线算法(Tarjan)和在线算法(ST)
- hihoCoder 1069 最近公共祖先 在线算法
- 最近公共祖先(LCA)算法实现过程 【Tarjan离线+倍增在线+RMQ】
- hdu5452 离线最近公共祖先
- 最近公共祖先lca离线
- 最近公共祖先(离线)
- hdu 2586 How far away ? 最近公共祖先lca 在线算法(倍增法)/离线算法(Tarjan算法)
- LCA(最近公共祖先)问题的离线算法
- 树上两点的最近公共祖先-Tarjan_LCA离线算法
- Tarjan离线算法求最近公共祖先(LCA)
- POJ1986 DistanceQueries 最近公共祖先LCA 离线算法Tarjan
- 【bzoj2157】旅游 LCT
- 前端客 中的各种tip
- 你真的会写单例模式吗——Java实现
- 用gsoap实现面向Java客户端的WebService
- vb.net DES加密与解密
- 最近公共祖先 朴素 离线 在线 算法合集
- SDCC 2017上海站讲师名单曝光 瞧一瞧有不有你心怡的嘉宾!
- gulp前端工程化教程
- CC2640R2F器件的核心架构与技术说明
- DialogFragment显示问题。
- react-native学习(1)
- Unity预计算全局光照的学习(速度优化,LightProbe,LPPV)
- 微信小程序页面跳转传值
- git 根据tag创建分支