(本博客大多参考ajh大佬)
1、什么是LCA
就是在一棵树中求两个节点的最近公共祖先。
例如:在下图中4和6的LCA为1,8和7的LCA为5。
2、怎么求LCA
首先我们先说怎么存储一个图
这里是链表结构(下面引用ajh大佬的代码)
struct Edge {//链式前向星存边 int to,nxt; /* to:该条边的终点 nxt:下一条边的的编号 */ }e[N<<1];//无向图要记得存两遍 int head[N],cnt;//head[i]表示以点i为起点的第一条边,cnt用于统计边数 inline void ade(int u,int v){ e[++cnt].to=v;//这里直接存即可 /* 下面两句是链式前向星的精髓 其实是和链表的插入一样的 例如,我们要在点u已有e1,e2...边的情况下加一条e0 也就是将e0插入到u->e1->e2->...->en->NULL这个链表里 这里我们选择在表头插入 */ e[cnt].nxt=head[u];//e0->e1 head[u]=cnt;//u->e0 /* 所以最后的链表就变成了: u->e0->e1->...->en->NULL */ }
接下来预处理求每个点的父亲和深度
void pre(int now,int f){//这里一定要记录上f dep[now]=dep[f]+1;//先预处理深度和父亲 fa[now]=f; /* 以下为链式前向星的遍历方式: 这里的i是边的编号 也就是now->e0->e1->...->en->NULL ^ | i 这样就可以一直往后遍历到NULL啦 */ for(rg int i=head[now];i;i=e[i].nxt){ int v=e[i].to; if(v!=f){ pre(v,now);//往下搜 } } } 然后来介绍几种求法(一共有5种求法,但我理解的不是很好,先说3种) (1)暴力 暴力的方式很容易想到就是一个一个往上跳看什么时候跳到相同的节点。 inline int LCA(int u,int v){ while(u!=v){ if(dep[u]>dep[v])u=fa[u];//暴力往上跳 else v=fa[v]; } return u; }
但暴力的时间复杂度非常地糟糕,一般时间会爆几个点。
(2)倍增
发现暴力跳的步数太多了,考虑减少步数。 于是我们的倍增算法应运而生
主要思想是利用二进制思想,一次跳多步 我们设f[i][j]为i向上跳2j步所到达的点,那么f[i][j]=f[f[i][j-1]][j-1](初始化f[i][0]=fa[i])
可以参考一下代码,
具体步骤如下(设此时求u和v的LCA):
1.预处理出f数组
2.将u、v跳到同一层
3.如果相等了那么直接返回
4.否则继续向上跳,直到它们都跳到LCA的下一层(具体原因可以想一想)
5.它们的父亲就是LCA
最主要的两段代码:
首先预处理要改一下
void DFS(int now,int f){ dep[now]=dep[f]+1;//这里的预处理和暴力几乎一样 fa[now][0]=f; for(rg int i=1;(1<<i)<=dep[now];i++){ fa[now][i]=fa[fa[now][i-1]][i-1];//更新fa数组 } for(rg int i=head[now];i;i=e[i].nxt){ int v=e[i].to; if(v!=f){ DFS(v,now); } } } inline int LCA(int u,int v){ if(dep[u]<dep[v])swap(u,v);//此处默认u比v深 while(dep[u]>dep[v]){//先跳到同一层 u=fa[u][lg2[dep[u]-dep[v]]-1]; } if(u==v)return u;//如果相等了一定要及时返回 for(rg int i=lg2[dep[u]-1];i>=0;i--){//注意要向下枚举,防止回溯 if(fa[u][i]!=fa[v][i]){ u=fa[u][i],v=fa[v][i];//跳跳跳 } } return fa[u][0];//返回跳2^0步即返回他们共同的父亲 }
(3)RMQ
在学习LCA转RMQ之前,我们先要介绍一下RMQ及它的求法:ST表 RMQ是什么?
Range Minimum/Maximum Query ST表是什么? Sparse-Table(说了等于没说)
思想:倍增+DP f[i][j]表示从i开始的2^j个元素的最值 转移方程:f[i][j]=min{f[i][j-1],f[i+2^(j-1)][j-1]} 公式很简单,结合图例感性理解下就好
那么这个ST表是怎样求出区间最值的呢?
画个图看下:由于分成的两个区间有重合并不影响求最值的结果,所以我们可以直接从左右两端分别找
容易知道,我们一定可以找到一个x,使得[l,l+2^x-1]和[r-2^x+1,r]覆盖整个区间[l,r] x怎么找?
说了这么多,我们到底怎么把LCA转成RMQ呢? 开启你们的脑洞,我们还是以此图为例: 写出它的欧拉序: 1 2 4 2 3 2 1 5 6 8 6 5 7 5 1 找不出规律?看看每个点的深度? 1 2 3 2 3 2 1 2 3 4 3 2 3 2 1 还是看4和8,把它们中间这段揪出来,发现了什么? 这段区间内深度最浅的点1就是4和8的LCA! 继续感性理解
注意事项:
1.哪里需要-1哪里不需要一定要分清楚
2.因为是欧拉序所以空间要开两倍
代码:
void DFS(int now,int f,int nowdep){ fst[now]=++tim;//tim为时间戳,此处只记录now第一次出现的时间 vis[tim]=now;//在tim时刻遍历到now dep[tim]=nowdep;//当前深度为nowdep for(int i=head[now];i;i=e[i].nxt){ int v=e[i].to; if(v!=f){ DFS(v,now,nowdep+1);//往下搜 vis[++tim]=now;//欧拉序回来也要记录一次! dep[tim]=nowdep; } } } void Init_RMQ(){ for(int i=1;i<=tim;i++){//还是先预处理log2,同时把fa也搞出来 lg2[i]=lg2[i-1]+(1<<lg2[i-1]==i); fa[i][0]=i; } /* 至于先枚举哪一个的问题,我是这么想的: 预处理的时候我们的第二维指数是固定的0 所以后面也只能一层层枚举指数才能保证之前的都枚举过了 */ for(int i=1;(1<<i)<=tim;i++){//外层枚举指数,内层枚举位置(!) for(int j=1;j+(1<<i)-1<=tim;j++){ if(dep[fa[j][i-1]]<=dep[fa[j+(1<<(i-1))][i-1]]){//比一下深度 fa[j][i]=fa[j][i-1];//和普通的st表一样记录就行了 }else fa[j][i]=fa[j+(1<<(i-1))][i-1]; } } } inline int LCA(int u,int v){ int r=fst[u],l=fst[v]; if(l>r)swap(l,r); int k=lg2[r-l+1]-1; if(dep[fa[l][k]]<=dep[fa[r-(1<<k)+1][k]]){ return vis[fa[l][k]]; }else return vis[fa[r-(1<<k)+1][k]]; }