文章目录
- LCA介绍
- 解决方法概括:
- 倍增法:
- Tarjan
- 欧拉序
- 树剖解法:
看了很多关于LCA的文章,这里是一个总结
我们通过这个题洛谷P3379 【模板】最近公共祖先来讲LCA
LCA介绍
lca是啥?最近公共祖先
就是:两个点在这棵树上距离最近的公共祖先节点
LCA(x,y)=z,z是x与y的最近公共祖先,(换句话说z的深度要尽可能大)
来看一个经典图
LCA(4,5)=2
LCA(4,3)=1
LCA(2,1)=1
解决方法概括:
常用四种方法 :
- 用倍增法求解,预处理复杂度是 O(nlogn) ,每次询问的复杂度是 O(logn), 属于在线解法。
- 利用欧拉序转化为RMQ问题,用ST表求解RMQ问题,预处理复杂度 O(n+nlogn),每次询问的复杂度为 O(1),也是在线算法。
- 采用Tarjan算法求解,复杂度 O(α(n)+Q),属于离线算法。
- 利用树链剖分求解,复杂度预处理O(n),单次查询 O(logn) ,属于在线算法。
倍增法:
倍增:将两个点调到一个高度之后不断同时向上调到两点重合,即为两点的最近公共祖先
所用到的函数: grand[x][i] ,这个数组表示标号为x节点向上跳2^i步的节点
例如grand[5][0]=2(上图), 节点5向上跳20次(1次)到达节点2
grand[5][0]就是x的父节点
grand[x][1]就是x的父亲节点的父亲节点,就是grand[grand[x][0]][0]
这样就能得到一个递推式grand [ x ] [ i ] = grand [ grand [ x ] [ i-1 ] ] [ i-1 ]
先让x与y处于同一层,然后一起往上跳
跳多少呢?
比如dep[u]>dep[v]
u要向上爬h=dep[u]-dep[v],才能和v相同深度
将h进行二进制拆分,比如
h=(15)10=(1111)2
h=(5)10=(101)2
从低位开始i=0,如果是这一位是1,就grand[u][i]
任何调动次数都可以用2的指数幂之和来表示O(log n)
h = 5 = 22 + 20
h = 15 = 23 + 22 + 21 + 20
dep[]表示节点的深度
一开始跳log2dep[x]-dep[y],log我们可以打表预处理。
lg[0]=-1;
for(int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;
当两点汇合时,就可以返回了
查询m组,总的复杂度应该是O(m log n)
输入:
9 1 1
1 2
1 3
2 4
2 5
4 7
4 8
3 6
6 9
5 8
2
#include<bits/stdc++.h>
#define maxn 500005
using namespace std;
int n, m, root;
int read()
{int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();};while(isdigit(ch)) x = (x << 3) + (x << 1) + ch - '0', ch = getchar();return x * f;
}struct edge
{int to, nxt;edge() {}edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1];int head[maxn], k = 0;
void add(int u, int v)
{e[k] = edge(v, head[u]);head[u] = k++;
}int fa[maxn][25], dep[maxn], lg[maxn];
void dfs(int now, int fath)//初始化深度及祖祖辈辈
{dep[now] = dep[fath] + 1;fa[now][0] = fath;for(int i = 1; (1 << i) <= dep[now]; i++)fa[now][i] = fa[fa[now][i - 1]][i - 1];//前文的递推式for(int i = head[now]; ~i; i = e[i].nxt)if(e[i].to != fath) dfs(e[i].to, now);//继续往下遍历
}int lca(int x, int y)
{if(dep[x] < dep[y]) swap(x, y);//保证x的深度更大,跳xwhile(dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]]];if(x == y) return x;//特判for(int i = lg[dep[x]]; i >= 0; i--)//倍增一起往上跳if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];return fa[x][0];
}int main()
{memset(head, -1, sizeof head);n = read(), m = read(), root = read();register int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);}dfs(root, 0);lg[0]=-1;
for(int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;
// for(int i = 1; i <= n; i++)
// lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);//log打表,后面那一坨是特判一下i是否进位了for(int i = 1; i <= m; i++){u = read(), v = read();printf("%d\n", lca(u, v));}return 0;
}
Tarjan
倍增是在线算法
Tarjan是离线算法
Tarjan主要用到和并查集差不多的方法
比如查询7和5
我们从根节点出发,1->2->4->7,发现另一点5还没被查询,然后回溯,再遍历,7->4->8->4->2->5,遍历到点5,我们发现另外一个点7已经访问过,就到此结束。在这过程中我们用fa[]来表示父节点,就和并查集一样,回溯时,7->4 , fa[7]=4 ;8->4 , fa[8]=4; 4->2,fa[4]=2; 5->2,fa[5]=2。
这样当发现另外一个点已经标记了,那么这个点的祖先一定是两个点的lca(可以通过路径压缩)
在访问一个点时,我们会将与这点相关的一同询问,所以tarjan是强制离线
lca用于存结果,每个询问都存了两次(因为还要查询另外一个点是否已经访问过),最后输出时 i * 2
每组答案lca[i*2](i->m)
大概时间复杂度为O(n+m)
#include<bits/stdc++.h>
#define maxn 500005
using namespace std;
int n, m, root, lca[maxn << 1];
int read()
{int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();return x * f;
}struct edge
{int to, nxt;edge(){}edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1], qe[maxn << 1];int head[maxn], k = 0;
void add(int u, int v)
{e[k] = edge(v, head[u]);head[u] = k++;
}int qhead[maxn], qk = 0;
void qadd(int u, int v)
{qe[qk] = edge(v, qhead[u]);qhead[u] = qk++;
}int fa[maxn];
int get(int x) {return fa[x] == x? x : fa[x] = get(fa[x]);}//记得路径压缩!!bool vis[maxn];
void tarjan(int u)
{register int v;vis[u] = 1;for(int i = head[u]; ~i; i = e[i].nxt)//先深优遍历下去{v = e[i].to;if(vis[v]) continue;//vis过了,就说明是父亲tarjan(v);fa[v] = u;//回溯时记录}for(int i = qhead[u]; ~i; i = qe[i].nxt)//开始扫一遍关于u的所有询问{v = qe[i].to;if(vis[v])//另一个点访问过了,可以得出答案了{lca[i] = get(v);if(i & 1) lca[i - 1] = lca[i];//这里特殊处理是因为每个询问存了两次else lca[i + 1] = lca[i];}}
}int main()
{memset(head, -1, sizeof head);memset(qhead, -1, sizeof qhead);n = read(), m = read(), root = read();register int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);fa[i] = i;//顺便初始化fa}fa[n] = n;for(int i = 1; i <= m; i++){u = read(), v = read();//存储询问qadd(u, v);qadd(v, u);}tarjan(root);//开始遍历for(int i = 0; i < m; i++)printf("%d\n", lca[i << 1]);//每个询问都存了两次,所以要*2
}
m较小时用倍增,较大时用Tarjan
欧拉序
欧拉序的定义
树在dfs过程中的节点访问顺序称为欧拉序.
那有人会问:dfs序和欧拉序啥区别?
dfs序:是指将一棵树被dfs时所经过的节点顺序(不绕回原点)。
欧拉序:就是从根结点出发,按dfs的顺序在绕回原点所经过所有点的顺序。
欧拉序与dfs序不同地方在于,欧拉序中每个节点可以出现多次,比如进入一次退出一次,又比如每次回溯时记录一次。
因此两个点的LCA,就是在该序列上两个点第一次出现的区间内深度最小的那个点
比如求D和E的LCA,D和E第一次出现的区间是DDCBE。这里面深度最小的就是点B,所以LCA(D,E)=B
这样就转化为区间RMQ问题,所以可以用ST表。
具体怎么做呢?
先求出欧拉序和每个节点的深度,同时用start[]记录每个节点第一次出现的位置
LCA ( T , u , v ) = RMQ ( B , start ( u ) , start ( v ) )
然后直接用ST表求RMQ就行
ST表详解
所用到数组:
a.欧拉序:图的遍历(几种存储结构写法不太一样)
cnt:序列长度(每个元素一进一出共两次,记得最大初始化为2*MAXN)
oula[]:欧拉序列,记录编号 dfs前记录一次,dfs后(回溯)再记录一次
depth[]:每个编号的深度(也可以记录每个下标的深度,见注释)start[]:每个编号第一次出现的序列下标
b.ST表
minl[i][j] 记得第一层初始化为depth[]
pos[][]最值下标,第一层初始化为i
注意这里i是欧拉序列的下标,最终要的是编号(这里经常下标搞混!!)
欧拉序列下标=start【编号】
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define N 500010
vector<int>G[N];
int oula[N<<1],depth[N],start[N];//N*2
int cnt;int minl[N<<1][19],pos[N<<1][19],len[N<<1],vis[N],tmp; //开20以上就TLE,自闭
//struct node{int deep,order;}minl[N][19];int n,m,s,x,y;inline int read()
{char ch='*';while(!isdigit(ch=getchar()));int num=ch-'0';while(isdigit(ch=getchar()))num=num*10+ch-'0';return num;
}inline void dfs(int now,int fa,int deep){oula[++cnt]=now;//入 //depth[cnt]=deep;if(depth[now]==0)depth[now]=deep;if(start[now]==0)start[now]=cnt;int z=G[now].size();for(int i=0;i<z;i++){if(G[now][i]!=fa){dfs(G[now][i],now,deep+1);oula[++cnt]=now;//出 //depth[cnt]=deep;//index[now]=cnt;}}
}inline void S_table(){for(int i=1;i<2*N;++i) len[i]=(1<<(tmp+1))==i?++tmp:tmp;for(int i=1;i<=cnt;i++) minl[i][0]= depth[oula[i]],pos[i][0]=i;//depth[i]//int l = log2((double)cnt);for (int j=1;(1<<j)<=cnt;j++){for (int i=1; i + (1 << (j-1) ) - 1 <=cnt;++i){//minl[i][j] = min(minl[i][j-1], minl[i + (1 << (j-1) )][j-1]);if(minl[i][j-1]<minl[i+(1<<(j-1))][j-1])minl[i][j]=minl[i][j-1],pos[i][j]=pos[i][j-1];else minl[i][j]=minl[i + (1 << (j-1) )][j-1],pos[i][j]=pos[i + (1 << (j-1) )][j-1];}}
}inline int rmq(int l, int r){if(l>r) swap(l,r);int k=len[r-l+1];//int k = log2((double)(r-l+1));int mid=r-(1<<k)+1;if(minl[l][k]<=minl[mid][k])return pos[l][k];else return pos[mid][k];
}
int main()
{n=read();m=read();s=read(); for(int i=1;i<n;++i){x=read();y=read();G[x].push_back(y);G[y].push_back(x);}dfs(s,-1,1);//求欧拉序列S_table();//初始化st表for(int i=1;i<=m;++i){x=read();y=read();printf("%d\n",oula[rmq(start[x],start[y])]);}return 0;
}
树剖解法:
树剖比Tarjan慢,但比倍增快
树剖详讲
树剖是把一棵树按子树大小分为链。树剖基本操作中有一个是求x到y的路径的边权和,或者是所有边权进行修改。我们可以用树剖的思路来写LCA。直接看点x和y是否在一条链上,不在则深度较大者跳到链头的父亲节点处,也就是跳出这条链;在则深度较浅者为LCA。
树剖一跳就是一条链,对于n极大的情况就相当于是倍增的再一优化。
#include<bits/stdc++.h>//都是树剖模板操作,就不做多解释了。
#define maxn 500005
using namespace std;
int n, m, root;
int read()
{int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();return x * f;
}
struct edge
{int to, nxt;edge(){}edge(int tt, int nn){to = tt, nxt = nn;}
}e[maxn << 1];int k = 0, head[maxn];
void add(int u, int v)
{e[k] = edge(v, head[u]);head[u] = k++;
}int fa[maxn], dep[maxn], size[maxn], son[maxn];
void dfs_getson(int u)
{size[u] = 1;for(int i = head[u]; ~i; i = e[i].nxt){int v = e[i].to;if(v == fa[u]) continue;dep[v] = dep[u] + 1;fa[v] = u;dfs_getson(v);size[u] += size[v];if(size[v] > size[son[u]]) son[u] = v;}
}int top[maxn];
void dfs_rewrite(int u, int tp)
{top[u] = tp;if(son[u]) dfs_rewrite(son[u], tp);for(int i = head[u]; ~i; i = e[i].nxt){int v = e[i].to;if(v != fa[u] && v != son[u]) dfs_rewrite(v, v);}
}void ask(int x, int y)
{while(top[x] != top[y])//不同则跳{if(dep[top[x]] > dep[top[y]]) swap(x, y);y = fa[top[y]];}if(dep[x] > dep[y]) swap(x, y);printf("%d\n", x);//输出深度较小者
}int main()
{memset(head, -1, sizeof head);n = read(), m = read(), root = read();int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);}//树剖初始化dfs_getson(root);dfs_rewrite(root, root);for(int i = 1; i <= m; i++){u = read(), v = read();ask(u, v);}return 0;
}