LCA今天看了一下 ,就是给你一颗树,让你求出两个点的最近公共祖先。如果对图遍历比较熟悉的话,感觉还是挺简单的。
先讲讲暴力思路吧,懂了暴力这个再用倍增优化会比较好理解。
暴力思路,首先维护两个数组:
1.depth[vertice]表示vertice的深度
2.father[vertice]表示vertice的上一个祖先
既然是最近公共祖先,那么我们先把两个点u、v的深度处理到一致,然后两个点一起往根遍历,直到u=v,此时u(v)就是开始时u、v的最近公共祖先。
暴力代码:
ll depth[500010];
ll head[500010];
ll father[500010];
ll ct = 1;
struct node
{
ll v, next;
} e[1000010];
void add(ll u, ll v) //建图
{
e[ct].v = v;
e[ct].next = head[u];
head[u] = ct++;
}
void dfs(ll now, ll fa, ll dep) //dfs处理depth和father
{
depth[now] = dep;
father[now] = fa;
for (int i = head[now]; i; i = e[i].next)
{
if (e[i].v != fa)
dfs(e[i].v, now, dep + 1);
}
}
ll lca(ll u, ll v)
{
while (depth[u] > depth[v])
u = father[u];
while (depth[v] > depth[u]) //将uv的深度处理到一致
v = father[v];
while (v != u)
{
v = father[v];
u = father[u];
}
return u;
}
int main()
{
ll n, m, s;
cin >> n >> m >> s;
for (int i = 1; i < n; i++)
{
ll u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs(s, 0, 1);
for (int i = 1; i <= m; i++)
{
ll u, v;
cin >> u >> v;
cout << lca(u, v) << endl;
}
return 0;
}
这样写单次询问的时间复杂度为
O
n
On
On,
m
m
m次询问就是
m
n
mn
mn,显然会:
接下来就用倍增优化。其实倍增优化很简单,就是一次尽可能往上多跳几次,在这里用2的倍数进行优化,也就是很常见的二进制优化。例如公共祖先要往上10层,我们就可以第一次直接跳跃8层,第二次直接跳跃2层完成(虽然看完下面你会知道实际上第二次直跳一层)。
同样,我们要处理depth数组,但是father数组我们增加到二维,father[i][j]表示顶点i往上的第 2 ( j − 1 ) 2^{(j-1)} 2(j−1)个祖先
例如上面的例题样例解释中,father[3][1]和father[5][1]都表示顶点4。father[3][0]和father[5][0]表示顶点1。
怎么样快速处理出完整的father数组呢?
我们可以递推。
在暴力写法中,我们已经知道可以在dfs中求出father[i][0]也就是顶点i的上一个祖先了。事实上,我们可以用father[i][0]递推出顶点i的所有需要的father数组信息。
看上图,dfs是从根结点开始搜索的,当我们处理father[3]时,4、2、1的father信息都已经知道了。
father[3][0]也已经在dfs到顶点3的时候就知道了,我们要做的就是递推出father[3][1]、father[3][2]、…father[3][n](当然,这题只需要处理到father[3][1])。
直接给出转移方程:father[i][j]=father[father[i][j-1]][j-1]
其实很好理解,自己想想就知道了,例如x的第2个祖先的第2个祖先是x的第4个祖先、x的第8个祖先的第8个祖先是x的第16个祖先。
另外有个细节问题:其实我们跳跃的最终目标不是最近公共祖先上,而是这个最近公共祖先的子节点,因为我们从大往小跳跃(比如要跳10步,我们先跳8再跳2),如果直接跳到最近公共祖先上,会出现16和4都是u和v的公共祖先,这样16就满足了,但显然不是最近的。因此我们跳跃到最近公共祖先的子节点。最后答案就是这个点的父节点。
算法思路讲的差不多了,直接上代码吧,有些细节不清楚的结合代码理解就好。
AC代码:
/*
* @Author: hesorchen
* @Date: 2020-07-03 17:05:01
* @LastEditTime: 2020-08-19 22:18:44
* @Description: https://hesorchen.github.io/
*/
#include <map>
#include <set>
#include <list>
#include <queue>
#include <deque>
#include <cmath>
#include <stack>
#include <vector>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define endl '\n'
#define PI acos(-1)
#define PB push_back
#define ll int
#define INF 0x3f3f3f3f
#define mod 1000000007
#define pll pair<ll, ll>
#define lowbit(abcd) (abcd & (-abcd))
#define max(a, b) ((a > b) ? (a) : (b))
#define min(a, b) ((a < b) ? (a) : (b))
#define IOS \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0);
#define FRE \
{ \
freopen("in.txt", "r", stdin); \
freopen("out.txt", "w", stdout); \
}
inline ll read()
{
ll x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
//head==============================================================================
ll depth[500010];
ll head[500010];
ll father[500010][35];
ll ct = 1;
struct node
{
ll v, next;
} e[1000010]; //双向边开两倍!双向边开两倍!双向边开两倍!
void add(ll u, ll v) //链式前向星加边
{
e[ct].v = v;
e[ct].next = head[u];
head[u] = ct++;
}
void dfs(ll now, ll fa, ll dep) //处理每个vertice的深度和第一个祖先
{
depth[now] = dep;
father[now][0] = fa;
for (int i = 1; (1 << i) <= depth[now]; ++i) //dfs到点now时顺便处理father数组
father[now][i] = father[father[now][i - 1]][i - 1];
for (int i = head[now]; i; i = e[i].next)
if (e[i].v != fa)
dfs(e[i].v, now, dep + 1);
}
ll n, m, s;
ll lca(ll u, ll v) //计算lca
{
if (depth[u] < depth[v])
swap(u, v);
ll cha = depth[u] - depth[v];
for (int i = 0; i <= 30; i++) //和暴力解法一样,先把两个点的深度处理到一致,至于为什么循环到30看下一条注释
if ((1 << i) & cha)
u = father[u][i];
if (v == u) //这种情况就是uv有一个是对方的祖先
return u;
for (int i = 30; i >= 0; i--) //这里我看别人代码用了log优化,其实数据在int范围内,不卡常的话从30开始足够了
if (father[u][i] != father[v][i])//注意。不相等才跳跃,因为我们的目标不是直接跳到最近公共祖先上。看上面的红字。
{
u = father[u][i];
v = father[v][i];
}
return father[u][0];
}
int main()
{
IOS;
cin >> n >> m >> s;
for (int i = 1; i < n; i++) //建图
{
ll u, v;
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs(s, 0, 1); //处理每个vertice的深度和祖先
for (int i = 1; i <= m; i++)
{
ll u, v;
cin >> u >> v;
cout << lca(u, v) << endl;
}
return 0;
}