LCA
LCA=Lowest Common Ancestor \textbf{LCA=Lowest Common Ancestor} LCA=Lowest Common Ancestor
即最近公共祖先
下文以 LCA(a,b)表示节点a与节点b的最近公共祖先 \textbf{LCA(a,b)表示节点a与节点b的最近公共祖先} LCA(a,b)表示节点a与节点b的最近公共祖先
F1: 暴力算法
步骤:
(1)求出每个节点的深度( s i z e size size)
(2)询问两个点是否重合,若重合,则 LCA(a,b)=当前重合的节点 \textbf{LCA(a,b)=当前重合的节点} LCA(a,b)=当前重合的节点
(3)否则,选择 max(size[a],size[b]) \textbf{max(size[a],size[b])} max(size[a],size[b]) 并移动到他的父亲节点
F2:倍增写法
倍增写法是基于F1暴力写法的优化版本,改进了时间复杂度
步骤:
(1)定义倍增数组 L C A [ M A X N ] [ M A X N L O G ] LCA[MAXN][MAXNLOG] LCA[MAXN][MAXNLOG] ,其中 L C A [ i ] [ j ] LCA[i][j] LCA[i][j] 表示 i i i 节点向上减少 2 j 2^j 2j 步可到达的节点
因为想要跳 2 j 2^j 2j 步可以先网上跳 2 j − 1 2^{j-1} 2j−1 步然后再站在 2 j − 1 2^{j-1} 2j−1 的地方再跳 2 j − 1 2^{j-1} 2j−1 步
由上可得公式:
LCA[i][j] \textbf{LCA[i][j]} LCA[i][j] = LCA[ LCA[i][j-1] ][ j-1 ] \textbf{LCA[ LCA[i][j-1] ][ j-1 ]} LCA[ LCA[i][j-1] ][ j-1 ]
即
2 j 2^j 2j = 2 j − 1 2^{j-1} 2j−1 + 2 j − 1 2^{j-1} 2j−1
(2)把 a , b a,b a,b 移动到同一深度,即 size[a]==size[b] \textbf{size[a]==size[b]} size[a]==size[b]
设 m a x a b = m a x ( s i z e [ a ] , s i z e [ b ] ) maxab=max(size[a],size[b]) maxab=max(size[a],size[b]) , m i n a b = m i n ( s i z e [ a ] , s i z e [ b ] ) minab=min(size[a],size[b]) minab=min(size[a],size[b])
则我们将 m a x a b maxab maxab向上跳 size[minab]-size[maxab] \textbf{size[minab]-size[maxab]} size[minab]-size[maxab] 步
并将这个步数表示成二进制,即 2 n 2^n 2n 步,其中
n= l o g 2 s i z e [ m i n a b ] − s i z e [ m a x a b ] log_2^{size[minab]-size[maxab]} log2size[minab]−size[maxab]
由此即可通过 倍增数组 ( LCA[MAXN] [MAXLOG] ) 来向上跳 2 i 2^i 2i 步
即可实现在时间复杂度为 O ( l o g 2 n ) O(log_2^n) O(log2n) 的情况下到达目标深度
(3)求出 L C A ( a , b ) LCA(a,b) LCA(a,b)
设 L L L 为 a a a 与 b b b 节点向上走的第 L L L 步后到达了同一节点,则此节点就是 L C A ( a , b ) LCA(a,b) LCA(a,b)
同理,节点 a a a 与 b b b 向上走的第 L − 1 L-1 L−1 步肯定为不同的节点
反之,向上走的第 L + 1 L+1 L+1 步就为节点 a a a 与节点 b b b 第二近公共祖先,次公共祖先
我们可以从大到小枚举往上走 2 i 2^i 2i 步,若节点 a a a 与 b b b 为同一节点则停止,否则一起向上走,直到根节点
算法代码:
int size[MAXN],LCA[MAXN][MAXLOG];
int fi[MAXN];//节点的后代个数
int to[MAXN];//节点的具体每个儿子
int nxt[MAXN];//nxt[i]表示节点i最后一条连出去的边
void dfs(int a,int father)
{
size[a]=size[father]+1;//子节点深度是父亲节点深度+1
LCA[a][0]=fa;//a节点向上走2^0 步为其父亲节点
for(int i=1;i<MAXLOG;i++)
{
LCA[a][i]=LCA[LCA[a][i-1]][i-1];//刚推的公式
}
for(int i=fi[a];i!=0;i=nxt[i])//类似于最短路中的链式向前星
{
if(to[i]!=father)
{
dfs(to[i],a);//递归调用
}
}
return;
}
int getLCA(int x,int y)
{
if(size[x]<size[y])//刚才我们是定义了maxab和minab,这里直接进行比较,选出深度较大的节点
{
swap(x,y);
}
for(int i=MAXLOG-1;i!=0;i--)
{
if(size[LCA[x][i]]>=size[y])
{
x=size[x][i];
}
}
if(x==y)
{
return x;//找到LCA了
}
for(int i=MAXLOG-1;i!=0;i--)
{
if(LCA[x][i]!=LCA[y][i])
{
x=LCA[x][i];
y=LCA[y][i];
}
}
return LCA[x][0];
}
F3:Tarjan 算法
步骤:
(1)使用 DFS 深搜整棵树,开始时每个节点都是一个独立的集合( 并查集 )
(2)DFS (x)时,每次访问完子树 y ,把集合 y 合并到 x 中
(3)当 x 的所有子节点都被访问结束后,标记 x 为 t r u e true true,即已被访问
(4)遍历所有关于 x 的询问 ( x , y ) (x,y) (x,y) ,如果 y 已被访问,则本访问答案为并查集的 f i n d ( y ) find(y) find(y)
代码实现:
string g[MAXN],q[MAXN];
int f[MAXN];
bool book[MAXN];
void init()
{
for(int i=1;i<=n;i++)
{
f[i]=i;
}
return;
}
int findfather(int x)
{
if(x==f[x])
{
return x;
}
f[x]=findfather(x);
return f[x];
}
void Union(int x,int y)//合并函数,将节点x所在的集合合并到集合y中
{
f[findfather(x)]=f[findfather(y)];
return;
}
void dfs(int x)
{
for(int i=0;i<g[x].size();i++)
{
dfs(g[x][i]);
Union(g[x][i],x);
}
book[x]=true;
for(int i=0;i<q[x].size();i++)
{
int m=q[]
}
return;
}