目录
介绍
今天来介绍一个c++中的算法:最近公共祖先LCA。
最近公共祖先是什么呢?就是给出在一棵没有环的树上的两个节点,求出它们的最近公共祖先,也可以理解成深度最深的公共祖先。这个算法用处很大,比如我们可以用它来求任意两个节点的距离。具体后面会讲。
暴力实现
我们该怎么实现呢?
首先,我们要知道一个结点的所有祖先,就是他的父亲、父亲的父亲、父亲的父亲的父亲......
然后,我们先用最暴力的思路想一下:公共祖先,那我只要把两个点的所有祖先都找出来,然后有重复的不就是公共祖先了吗。至于最近,我们只需要将第一个重复的祖先输出就行了。
所以我们有了最暴力的做法:先把一个点的所有祖先找到并做上标记,然后找一个点的所有祖先,发现有标记就成功找到了最近公共祖先。
不过这种算法是很劣的,时间复杂度是(n)的,一般情况下都过不了
。所以我们就得想办法优化。
优化
怎么优化呢?下面我们就要引入一个知识:倍增。
我们可以看到,其实我们没有必要把所有的祖先都枚举做上标记,这样是很浪费时间的。举个例子,如果一个悬崖边上有一个宝箱,但你又不知道你距离悬崖口有多远,这时候你一步一步往前走肯定是很累的。假设你距离悬崖边13步,而你只有在距离悬崖边一步时才能看见悬崖边。这时候我们就得想一个既有效率又安全的走法。
我们可以抛石子来确定距离。第一次抛16步的距离,石子掉下去了。第二次抛8步的距离,石子没掉下去,那我们就往前走八步。第三次抛4步远的距离,我们发现可以往前走四步。现在距离悬崖只有1步了,可以发现悬崖和宝藏就在我们眼前。这样本来要走13步的路程,我们只走了3步,如果运气好第一次就抛8步那么就只要走两步。
向上面每次将距离除以2的方法就是倍增。
为什么这个方法可行呢?我们可以用2进制来解释。我们知道,所有十进制数都可以用二进制来表示,那么所有的距离我们都用二进制来表示。那我们就可以把每次调的距离当作加数,把问题转化成求几个二进制数相加等于一个二进制数。比如说 n = 1100101(2)这个数。
我们定第一个加数为A1 = 1000000,发现没有超过n,那就将加数除以二,也就是看下一位。加数变成了A2 = 100000,加上A1,没有超过n,除以二,看下一位。A3等于10000,加上前两个加数,现在的家属和变成了1110000,超过了n,所以这一位必须等于零,加数除以二。以此类推,我们分别加上了1000000,100000,100和1。这就代表我们在十进制中加上了32,16,4和1。这也就说明了每一次跳的距离都要除以二。
但是为什么要从大到小呢?为了方便理解,我们再回到悬崖上。还记得距离是13吧,在二进制中就是1101。
如果从小到大,第一次,我们选择的是1,发现可以。
第二次,我们跳2,也就是10,发现没有超过1101,但是1101中这一位却是0,我们就跳多了,到后面还需要回溯,也就是往回跳,明显不是最优。
所以在不知道具体距离的情况下我们需要从大往小减小距离。
倍增的时间复杂度是(logn)的,完完全全可以过。知道了这个前置知识,我们就可以开始学习LCA了。
例题
我们结合例题来详细讲解此算法吧。
复制一个题目:
题目描述
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
输入格式
第一行包含三个正整数 N,M,S 分别表示树的结点个数、询问的个数和树根结点的序号。
接下来 N-1 行每行包含两个正整数 x, y 表示 x 结点和 y 结点之间有一条直接连接的边(数据保证可以构成树)。
接下来 M 行每行包含两个正整数 a, b 表示询问 a 结点和 b 结点的最近公共祖先。
输出格式
输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。
输入输出样例
输入 #1
5 5 4 3 1 2 4 5 1 1 4 2 4 3 2 3 5 1 2 4 5
输出 #1
4 4 1 4 4
说明/提示
对于 30% 的数据,N≤10,M≤10。
对于 70% 的数据,N≤10000,M≤10000。
对于 100% 的数据,1≤N,M≤500000,1≤x,y,a,b≤N,不保证 a≠b。
首先,我们需要存图。很多大佬喜欢用前向星存图,但是前向星比 vector 其实快不了多少。我的建议是考试时第一遍先用 vector 寸土,写完后离考试结束还有充足的时间,那就再用前向星改一遍。因为 vector 写起来代码量要比前向星少很多,并且还很方便查错。(主要还是因为我懒)。但是考试时一定要选择好方法,万一(无良)出题人就想卡 vector 呢。
这里我们就用 vector 存图。
cin >> n >> m >> s;
for(int i = 1; i < n; i++){
cin >> x >> y;
edge[x].push_back(y);
edge[y].push_back(x);
}
由于不知道树长什么样,所以我们需要正过来存一编,反过来再存一遍。也就是把它当作无向图来存储。但这有可能出现一个问题,就是一个点的儿子有可能成为它的父亲,则时候就需要特判了,后面再讲。
然后我们需要把所有结点的祖先都找出来,就是用 dfs 遍历一遍树,用 fa[i][j] 表示节点 i 向上 2^j 级父亲,具体解释如图。
这样我们就可以表示每个节点 2^i 级的父亲了,这也方便后面的倍增。
void dfs(int now, int father){
deep[now] = deep[father] + 1;
for(int i = 1; (1 << i) <= deep[now]; i++){//i从1开始! 2^i <= 现在的深度,代表最高一级的父亲不能超过根节点
fa[now][i] = fa[fa[now][i - 1]][i - 1];
}
for(int i = 0; i < edge[now].size(); i++){
if(father != edge[now][i]){//注意!!now的儿子不能再回到now的父亲了
fa[edge[now][i]][0] = now;
dfs(edge[now][i], now);
}
}
}
首先,当前的节点深度是父亲节点深度加1。然后获得 i 节点的所有 2^i 祖先节点。从 2^1 祖先开始,一直到根节点,也就是 2^i ≤ deep[now]。
那为什么 fa[now][i] = fa[fa[now][i - 1]][i - 1] 呢?我们知道,2^(i - 1) + 2^(i - 1) = 2^i,所以用 now 的 2^(i - 1) 级父亲的 2^(i - 1) 级父亲就得到了 now 的 2^i 级祖先。
我们从根节点开始深搜,就可以获得每个结点所需要的数据了。其中 deep 数组存储的是每个节点的深度。
然后就是求两个给定节点的最近公共祖先。
for(int i = 1; i <= m; i++){
cin >> x >> y;
cout << LCA(x, y) << endl;
}
LCA() 函数该怎么写呢?我们有了倍增的思路,那具体该怎么实现呢?首先,我们需要把两个节点的深度调到同一高度,这样做就可以让两个节点一起向上倍增,很方便。
让两个结点的深度相同,我们就把深度更深点的向上调。首先需要获取两个节点的深度差。
if(deep[x] < deep[y]) swap(x, y);
int h = deep[x] - deep[y];
然后不断让 x = fa[x][i]。
for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级
if(h & 1){
x = fa[x][i];
}
}
有人就要问了,那么这次为什么可以从小到大呢?因为我们已经知道了 h 的具体值,这样就可以准确地知道第几次跳,第几次不跳,具体做法就是每次用 h&1,在二进制中如果 h 的这一位是1,那么按位与够的结果就是1,就代表可以跳,并将 h 右移一位。i 就是指数。这样就实现了将 x 向上调高度与 y 相平。
然后将两个点一起向上倍增。首先要特判,如果两个点在同一高度后重合了,那么就说明它本身就是最近的公共祖先,直接返回 x 就好了
if(x == y) return x;
然后就从大到小向上。但是最大是多少呢?题目中说 1≤N≤500000 ,所以 2^i 最多不超过500000,也就是说 i 不超过20。所以循环起点就有了。
for(int i = 20; i >= 0; i--){
if(fa[x][i] != fa[y][i]){
x = fa[x][i];
y = fa[y][i];
}
}
咦,怎么 (fa[x][i] != fa[y][i]) 呢?因为如果向上直接就相等了,那么我们并不知道是否是最近的共公共祖先,所以如果相等那也不能加。
到了最后该怎么办呢?我们直接返回 fa[x][0] 就好了,为什么呢?其实所有二进制数最后一位只有 1 或 0(废话)。那么如果是1的话,由于循环中的判断条件,最后一位不会跳,因为跳了就相等了。如果是0的话,那么最后一个是1的一位不会跳,因为如果跳了就相等了,那么后面所有0都会跳。比如:10100。倒数第三位不能跳,后两位必须跳。那么就变成了:10011,可以看到,跳的数比原数要小一。
综上所述,两个情况下都要比真正的距离小一。所以我们只要返回 fa[x][0]。
所以LCA()函数就变成了这样:
int LCA(int x, int y){
if(deep[x] < deep[y]) swap(x, y);
int h = deep[x] - deep[y];
for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级
if(h & 1){
x = fa[x][i];
}
}
if(x == y) return x;
for(int i = 20; i >= 0; i--){
if(fa[x][i] != fa[y][i]){
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
这样,我们就能求得任意两个节点的最近公共祖先了。
总结
#include<bits/stdc++.h>
using namespace std;
const int N = 500010;
int n, m, s, fa[N][22], deep[N], x, y;
vector<int>edge[N * 2];
void dfs(int now, int father){
deep[now] = deep[father] + 1;
for(int i = 1; (1 << i) <= deep[now]; i++){//i从1开始!
fa[now][i] = fa[fa[now][i - 1]][i - 1];
}
for(int i = 0; i < edge[now].size(); i++){
if(father != edge[now][i]){//注意!!now的儿子不能再回到now的父亲了
fa[edge[now][i]][0] = now;
dfs(edge[now][i], now);
}
}
}
int LCA(int x, int y){
if(deep[x] < deep[y]) swap(x, y);
int h = deep[x] - deep[y];
for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级
if(h & 1){
x = fa[x][i];
}
}
if(x == y) return x;
for(int i = 20; i >= 0; i--){
if(fa[x][i] != fa[y][i]){
x = fa[x][i];
y = fa[y][i];
}
}
return fa[x][0];
}
int main(){
cin >> n >> m >> s;
for(int i = 1; i < n; i++){
cin >> x >> y;
edge[x].push_back(y);
edge[y].push_back(x);
}
dfs(s, 0);
for(int i = 1; i <= m; i++){
cin >> x >> y;
cout << LCA(x, y) << endl;
}
return 0;
}
这是LCA的模板,建议收藏。
我们再来总结一下。
首先,用 vector 存图,由于不知道树长什么样,所以要当成无向图来存储。
然后用深搜得到所有节点的深度和 2^i 父亲。注意:由于存图时存了两个方向,所以一个节点的儿子有可能一跃成为他的父亲,所以需要判断一下。
if(father != edge[now][i]){//注意!!now的儿子不能再回到now的父亲了
fa[edge[now][i]][0] = now;
dfs(edge[now][i], now);
}
然后找两个节点的最近公共祖先。先把 x 变为两个节点中深度更深的那一个,方便操作。获得两个点的深度差后将 x 的深度变成和 y 一样的。
for(int i = 0; h; i++, h >>= 1){//例如h = (1011)2,那么第一次要跳,并且跳2^1也就是2^0级
if(h & 1){
x = fa[x][i];
}
}
记住特判!如果 x 本身就是最近公共祖先,直接返回。
然后将 x 和 y 同时向上,如果有公共祖先就将距离除以2。最后返回 fa[x][0],也就是 x 的父亲。
特殊用处
其实LCA还有很多特殊用处,这里只介绍一种,就是它可以求出任意树上两个点的最短距离。
mindis(a, b) = deep(a) + deep(b) - 2 * deep(LCA(a, b))
为什么呢?我们可以这样理解,假设 q 点是 a、b 的最近公共祖先,那么就有
mindis(a, b) = deep(a) - deep(q) + deep(b) - deep(q)
就像这样。所以我们求出了两点的最近公共祖先,进一步就可以求出两点之间的最短距离。
LCA还有很多实现方法和用处,这里就不一一阐述。
如果对于LCA()还有不懂的,可以来评论区问我。