You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

429 lines
13 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

## [$P3379$ 【模板】最近公共祖先($LCA$](https://www.luogu.com.cn/problem/P3379)
#### $LCA$常见的四种求法
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230725082008.png)
### 一、暴力
> 操作步骤:
> ① 求出每个结点的深度
> ② 询问两个结点是否重合,若重合,则$LCA$已经求出
> ③ 否则,选择两个点中深度较大的一个,并移动到它的父亲
```cpp {.line-numbers}
int depth[N];
int fa[N];
void bfs(int root) {
queue<int> q;
q.push(root);
depth[root] = 1;
fa[root] = -1;
while (q.size()) {
int u = q.front();
q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!depth[v]) {
depth[v] = depth[u] + 1;
fa[v] = u;
q.push(v);
}
}
}
}
int lca(int a, int b) {
if (depth[a] > depth[b]) swap(a, b);
while (depth[a] < depth[b]) b = fa[b];
while (a != b) a = fa[a], b = fa[b];
return a;
}
```
![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/%7Byear%7D/%7Bmonth%7D/%7Bmd5%7D.%7BextName%7D/20230725084122.png)
### 二、倍增求$LCA$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 500010, M = N << 1;
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int depth[N];
int f[N][20];
void bfs(int root) {
queue<int> q;
q.push(root);
depth[root] = 1;
while (q.size()) {
int u = q.front();
q.pop();
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (!depth[v]) {
depth[v] = depth[u] + 1;
f[v][0] = u;
q.push(v);
for (int k = 1; k <= 19; k++) f[v][k] = f[f[v][k - 1]][k - 1];
}
}
}
}
int lca(int a, int b) {
if (depth[a] < depth[b]) swap(a, b);
for (int k = 19; k >= 0; k--)
if (depth[f[a][k]] >= depth[b]) a = f[a][k];
if (a == b) return a;
for (int k = 19; k >= 0; k--)
if (f[a][k] != f[b][k])
a = f[a][k], b = f[b][k];
return f[a][0];
}
int main() {
#ifndef ONLINE_JUDGE
freopen("P3379.in", "r", stdin);
#endif
memset(h, -1, sizeof h);
// Q:为什么上面的数组开20每次k循环到19
// 答: cout << log2(500000) << endl;
int n, m, s;
scanf("%d %d %d", &n, &m, &s);
int a, b;
for (int i = 1; i < n; i++) {
scanf("%d %d", &a, &b);
add(a, b), add(b, a);
}
bfs(s);
while (m--) {
scanf("%d %d", &a, &b);
cout << lca(a, b) << endl;
}
return 0;
}
```
### 三、$Tarjan$算法求$LCA$
什么是$Tarjan$(离线)算法呢?顾名思义,就是在一次遍历中把所有询问一次性解决,所以其时间复杂度是$O(n+q)$。
$Tarjan$算法的优点在于 **相对稳定**,时间 **复杂度也比较居中**,也很 **容易理解**。
下面详细介绍一下$Tarjan$算法的基本思路:
> 1. **任选一个点为根节点**,从根节点开始
> 2. 记$u$已被访问过
> 3. 遍历该点$u$所有子节点$j$
> 4. 若是$j$还有子节点,返回$3$,否则下一步
> 5. 合并$j$到$u$家族
> 6. 寻找与当前点$u$有询问关系的点$v,id$,
> 7. 若是$v$已经被访问过了,$lca[id]=find(v)$
遍历的话需要用到$dfs$来遍历(我相信来看的人都懂吧...),至于合并,最优化的方式就是利用 **并查集** 来合并两个节点。
下面上伪代码:
```cpp {.line-numbers}
//mergefind为并查集合并函数和查找函数
tarjan(u){
st[u] = true;
for each(u,v){ //访问所有u子节点v
tarjan(v); //继续往下遍历
p[v] = find(u);//合并vu
}
for each(u,v){ //访问所有和u有询问关系的v
如果v被访问过;
u,v的最近公共祖先为find(v);
}
}
```
个人感觉这样还是有很多人不太理解,所以我打算模拟一遍给大家看。
<font color='red' size=4><b>建议拿着纸和笔跟着我的描述一起模拟!!</b></font>
假设我们有一组数据 $9$个节点 $8$条边 联通情况如下:
$1-21-32-42-53-65-75-87-9$ 即下图所示的树
设我们要查找最近公共祖先的点为$9-84-67-55-3$
设$f[]$数组为并查集的父亲节点数组,初始化$f[i]=ivis[]$数组为是否访问过的数组,初始为$0$;
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005085438253-584479271.jpg'></center>
下面开始模拟过程:
取$1$为 **根节点****往下搜索** 发现有两个儿子$2$和$3$
先搜$2$,发现$2$有两个儿子$4$和$5$,先搜索$4$,发现$4$没有子节点,则寻找与其有关系的点;
发现$6$与$4$有关系,但是$vis[6]=0$,即$6$还没被搜过,所以 **不操作**
发现没有和$4$有询问关系的点了,返回此前一次搜索,**更新$vis[4]=1$**
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005091215206-185829617.jpg'></center>
表示$4$已经被搜完,更新$f[4]=2$,继续搜$5$,发现$5$有两个儿子$7$和$8$;
先搜$7$,发现$7$有一个子节点$9$,搜索$9$,发现没有子节点,寻找与其有关系的点;
发现$8$和$9$有关系,但是$vis[8]=0$,即$8$没被搜到过,所以**不操作**
发现没有和$9$有询问关系的点了,返回此前一次搜索,更新$vis[9]=1$
表示$9$已经被搜完,更新$f[9]=7$,发现$7$没有没被搜过的子节点了,寻找与其有关系的点;
发现$5$和$7$有关系,但是$vis[5]=0$,所以 **不操作**
发现没有和$7$有关系的点了,返回此前一次搜索,更新$vis[7]=1$
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005092745612-1059188156.jpg'></center>
表示$7$已经被搜完,更新$f[7]=5$,继续搜$8$,发现$8$没有子节点,则寻找与其有关系的点;
发现$9$与$8$有关系,此时$vis[9]=1$,则他们的最近公共祖先为$find(9)=5$
(`find(9)`的顺序为`f[9]=7-->f[7]=5-->f[5]=5 return 5;`)
发现没有与$8$有关系的点了,返回此前一次搜索,更新$vis[8]=1$
表示$8$已经被搜完,更新$f[8]=5$,发现$5$没有没搜过的子节点了,寻找与其有关系的点;
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005094054237-333460930.jpg'></center>
发现$7$和$5$有关系,此时$vis[7]=1$,所以他们的最近公共祖先为$find(7)=5$
($find(7)$的顺序为`f[7]=5-->f[5]=5 return 5;`)
又发现$5$和$3$有关系,但是$vis[3]=0$,所以不操作,此时$5$的子节点全部搜完了;
返回此前一次搜索,更新$vis[5]=1$,表示$5$已经被搜完,更新$f[5]=2$
发现$2$没有未被搜完的子节点,寻找与其有关系的点;
又发现没有和$2$有关系的点,则此前一次搜索,更新$vis[2]=1$
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005100146690-1294857778.jpg'></center>
表示$2$已经被搜完,更新$f[2]=1$,继续搜$3$,发现$3$有一个子节点$6$
搜索$6$,发现$6$没有子节点,则寻找与$6$有关系的点,发现$4$和$6$有关系;
此时$vis[4]=1$,所以它们的最近公共祖先为$find(4)=1$;
($find(4)$的顺序为`f[4]=2-->f[2]=2-->f[1]=1 return 1;`)
发现没有与$6$有关系的点了,返回此前一次搜索,更新$vis[6]=1$,表示$6$已经被搜完了;
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005101758206-890675418.jpg'></center>
更新$f[6]=3$,发现$3$没有没被搜过的子节点了,则寻找与$3$有关系的点;
发现$5$和$3$有关系,此时$vis[5]=1$,则它们的最近公共祖先为$find(5)=1$
($find(5)$的顺序为`f[5]=2-->f[2]=1-->f[1]=1 return 1;`)
发现没有和$3$有关系的点了,返回此前一次搜索,更新$vis[3]=1$
<center><img src='https://images2015.cnblogs.com/blog/818487/201510/818487-20151005102559159-375592206.jpg'></center>
更新$f[3]=1$,发现$1$没有被搜过的子节点也没有有关系的点,此时可以退出整个$dfs$了。
经过这次$dfs$我们得出了所有的答案,有没有觉得很神奇呢?是否对$Tarjan$算法有更深层次的理解了呢?
#### $Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 500010, M = N << 1;
//链式前向星
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
vector<PII> query[N]; // query[u]: first:询问的另一个顶点; second:询问的编号
int n, m, s;
int p[N]; // 并查集数组
bool st[N]; // tarjan算法求lca用到的是否完成访问的标识
int lca[N]; // 结果数组
int find(int x) {
if (p[x] != x) p[x] = find(p[x]); //路径压缩
return p[x];
}
void tarjan(int u) {
// ① 标识u已访问
st[u] = true;
//② 枚举u的临边tarjan没有访问过的点
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (!st[j]) {
tarjan(j);
//③ 完成访问后j点加入u家族
p[j] = u;
}
}
//④ 每个已完成访问的点,记录结果
for (auto q : query[u]) {
int v = q.first, id = q.second;
if (st[v]) lca[id] = find(v);
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d %d %d", &n, &m, &s);
int a, b;
for (int i = 1; i < n; i++) {
scanf("%d %d", &a, &b);
add(a, b), add(b, a);
}
for (int i = 1; i <= m; i++) {
scanf("%d %d", &a, &b);
query[a].push_back({b, i});
query[b].push_back({a, i});
}
//初始化并查集
for (int i = 1; i <= n; i++) p[i] = i;
tarjan(s);
//输出答案
for (int i = 1; i <= m; i++) printf("%d\n", lca[i]);
return 0;
}
```
### 四、RMQ+ST
挖坑待填
https://blog.csdn.net/m0_60630928/article/details/123160718?share_token=94367124-164f-4ecb-bddb-be496df1a2cd    
```cpp {.line-numbers}
//3.95s / 227.49MB / 1.67KB C++14 (GCC 9) O2
#include<bits/stdc++.h>
using namespace std;
const int N=500005;
vector<int>vec[N];
//记录每个结点可以走向哪些结点
int f[N*2][21],mem[N*2][21],depth[N*2],first[N],vis[N*2],lg[N];
//f:记录深度序列区间中的最小深度
//mem:记录 找到深度序列区间中的最小深度 时的对应结点(在欧拉序列中)
//depth:在dfs过程中记录遍历到每个点时的对应深度
//first:记录每个结点第一次出现时在欧拉序列中的位置
//vis:欧拉序列
//lglg[i]==log_{2}{i}+1
int cnt=0,n,m,s;
//cnt:每走到一个点计一次数
inline int read()
{
int 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*10+ch-48;ch=getchar();}
return x*f;
}
void dfs(int now,int dep)
{
if(!first[now]) first[now]=++cnt;//第一次遍历到该点
depth[cnt]=dep,vis[cnt]=now;
for(int i=0;i<vec[now].size();i++)
{
if(first[vec[now][i]]) continue;//是该结点的父节点,跳过
else dfs(vec[now][i],dep+1);
++cnt;
depth[cnt]=dep,vis[cnt]=now;//深搜完了vec[now][i]下的分支,回到当前结点 now
}
}
void RMQ()
{
for(int i=1;i<=cnt;i++)
{
lg[i]=lg[i-1]+((1<<lg[i-1])==i);
f[i][0]=depth[i];//区间长度为1时,该区间内深度的最小值就是该结点的深度
mem[i][0]=vis[i];
}
for(int j=1;(1<<j)<=cnt;j++)//枚举的区间长度倍增
for(int i=1;i+(1<<j)-1<=cnt;i++)//枚举合法的每个区间起点
{
if(f[i][j-1]<f[i+(1<<(j-1))][j-1])//深度最小的点在前半个区间
{
f[i][j]=f[i][j-1];
mem[i][j]=mem[i][j-1];
}
else//深度最小的后半个区间
{
f[i][j]=f[i+(1<<(j-1))][j-1];
mem[i][j]=mem[i+(1<<(j-1))][j-1];
}
}
}
int ST(int x,int y)
{
int l=first[x],r=first[y];//找到输入的两个结点编号对应在欧拉序列中第一次出现的位置
if(l>r) swap(l,r);
int k=lg[r-l+1]-1;
if(f[l][k]<f[r-(1<<k)+1][k]) return mem[l][k];
else return mem[r-(1<<k)+1][k];
}
int main()
{
n=read(),m=read(),s=read();
for(int i=1;i<n;i++)
{
int a,b;
a=read(),b=read();
vec[a].push_back(b);
vec[b].push_back(a);
}
dfs(s,0);//打表,给firstdepthvis赋值,给RMQ奠定基础
/*cout<<"各结点第一次出现的位置:"<<endl;
for(int i=1;i<=n;i++)
cout<<first[i]<<' ';
cout<<endl;
cout<<"欧拉序列:"<<endl;
for(int i=1;i<=2*n;i++)
cout<<vis[i]<<' ';
cout<<endl;
cout<<"深度序列"<<endl;
for(int i=1;i<=2*n;i++)
cout<<depth[i]<<' ';
cout<<endl;*/
RMQ();//打表,给fmem赋值 ,给ST奠定基础
while(m--)
{
int x,y;
x=read(),y=read();
printf("%d\n",ST(x,y));
}
return 0;
}
```