NOIP模板复习(2) LCA的三种解法
LCA还是图论中蛮重要的部分,解法众多,这里只拿三个比较常用的板子出来说说
目录
1.树上倍增
1.1算法原理
1.2算法实现
2.Tarjan算法
2.1算法原理
2.2算法实现
3.RMQ实现
3.1算法原理
3.2算法实现
4.总解
1.树上倍增
树上倍增,顾名思义是利用了倍增的思想实现的在线的LCA算法,具体来讲就是利用\(2^i\)可以相加组成任何数的原理实现的。
1.1算法原理
倍增算法主要是利用\(2^i\)可以相加组成任何数这一性质来组织信息转移状态,具体就是维护一个倍增数组\(f[i][j]\)表示编号为\(i\)的节点向上跳\(2^j\)步所到达的点。通过简单的思考我们可以发现节点\(i\)向上跳\(2^j\)步的节点是\(i\)号节点向上跳\(2^{j-1}\)步的节点再向上跳\(2^{j-1}\)步所到达的节点,因此我们可以得到一个式子:\(f[i][j]=f[f[i][j-1]][j-1]\),通过这个式子,我们可以轻易的在很短的时间内推出倍增数组。
而有了倍增数组就好办了,我们可以利用dfs求出每个节点距根节点的深度,然后我们便可以先将两个节点上跳到同一高度(注:以后上跳都可以利用倍增数组完成),再将两个节点同时上跳。当两个节点的祖先都相同时,便找到了两个节点的最近公共祖先。
该算法单个查询的时间复杂度为\(O(log(n))\)。
1.2算法实现
代码如下
#include
#include
#include
#include
#include
#include
#include
using namespace std;
vector tree[1005];
int deep[1005];
int anc[1005][25];
int father[1005];
void dfs(int root)//预处理出倍增数组和深度
{
anc[root][0]=father[root];
for(int i=1;i<20;i++)
{
anc[root][i]=anc[anc[root][i-1]][i-1];
}
int len=tree[root].size();
for(int i=0;i=0;i--)//将两个点调整到同一高度
{
if(deep[b]<=deep[anc[a][i]])
{
a=anc[a][i];
}
}
if(a==b)
{
return a;
}
for(int i=19;i>=0;i--)
{
if(anc[a][i]!=anc[b][i])//向上倍增寻找公共祖先
{
a=anc[a][i];
b=anc[b][i];
}
}
return anc[a][0];
}
int main()
{
int n,m,t;
scanf("%d %d %d",&n,&m,&t);
register int a,b;
for(int i=1;i<=m;i++)
{
scanf("%d %d",&a,&b);
tree[a].push_back(b);
tree[b].push_back(a);
}
father[1]=1;
dfs(1);
for(int i=1;i<=t;i++)
{
scanf("%d %d",&a,&b);
cout<
2.Tarjan算法
tarjan算法是一个利用dfs遍历和回溯的一个非常巧妙的方法,能在一次遍历内求出任意两点间的LCA的离线算法。
2.1算法原理
容易知道,当查询的两个节点u,v在同一棵子树内的时候,距离该子树的根节点最近的点就是LCA。而当两个节点不在同一子树内的时候,则这两个子树所在的子树的根节点就是LCA。
而这些信息我们都可以在dfs的搜索和回溯通过维护一个集合信息得到。具体步骤如下:
1.设定u为已访问。
2.设定u的祖先为u自身。
3.遍历u的所有邻接点v。
4.若未访问过,则dfs(v),合并u所在集合和v所在集合为一个新集合,设定新集合的祖先为u若访问过则不再访问。
5.检查跟这个u点有关的查询(u,v),若v已访问,则lca= v所在集合的祖先,若v未访问不做处理。
如果上面没理解的话可以看一下下面的代码。
2.2算法实现
代码如下
#include
#include
#include
#include
#include
#include
#include
using namespace std;
vector tree[1005];
vector ask[1005];
int father[1005];
int anc[1005];
bool used[1005];
void pre(int n)
{
for(int i=1;i<=n;i++)
{
father[i]=i;
tree[i].clear();
ask[i].clear();
}
memset(used,0,sizeof(used));
return ;
}
int find(int x)
{
if(x==father[x])
{
return x;
}
return father[x]=find(father[x]);
}
void unions(int x, int y)
{
x=find(x);
y=find(y);
if(x==y)
{
return ;
}
father[x]=y;
return ;
}
void tarjan(int root)
{
anc[root]=root;
used[root]=1;
int len=tree[root].size();
for(int i=0;i
3.RMQ实现
RMQ(Range Minimum/Maximum Query)问题指的是区间最值问题,通过使用DFS获得树节点的时间戳,便可以利用RMQ算法预处理后做到在线\(O(1)\)的查询LCA。
3.1算法原理
首先有一个显而易见的事实,两个节点的深度最深的祖先便是他们的最近公共祖先。而通过利用DFS标记时间戳,我们便可以在一个区间内知道这两个节点的全部祖先的信息。
而RMQ算法通常是使用ST表(Sparse Table)实现,具体实现是用\(f[i][j]\)来表示\([i,i+2^{j-1}]\)的区间的最值。而通过动态规划我们便可以预处理出\(f\)数组。首先\(f[i][0]\)的值就是它本身,而\(f[i][j]\)可以分为\((i,i+2^{j-1}-1)\)和\((i+2^{j-1},i+2^j-1)\)两段区间使得两段区间长度都为\(2^{j-1}\)。这样便可以得到状态转移方程\(f[i][j]=max(f[i][j-1],f[i+2^{j-1}][j-1])\)。
而RMQ的查询可以通过一个中间值\(k=log_{2}(j-i+1)\),则区间\((i,j)\)的最值就为\(max(f[i][k],f[j-2^k+1][k])\)。这样便可以在\(O(1)\)的时间内查询区间的最值了。
3.2算法实现
代码如下
#include
#include
#include
#include
#include
#include
#include
using namespace std;
vector tree[1005];
int cnt=1;
int st[1005<<1][20];
int deep[1005<<1];
int id[1005];
int idx[1005<<1];
void getST(int n)//预处理出ST表
{
for(int i=1;i<=n;i++)
{
st[i][0]=i;
}
for(int j=1;(1<r)
{
swap(l,r);
}
return idx[ask(l,r)];
}
int main()
{
int n,m,t;
scanf("%d %d %d",&n,&m,&t);
register int a,b;
for(int i=1;i<=m;i++)
{
scanf("%d %d",&a,&b);
tree[a].push_back(b);
tree[b].push_back(a);
}
dfs(1,-1,0);
getST(2*n);
for(int i=1;i<=t;i++)
{
scanf("%d %d",&a,&b);
cout<
4.总结
上面的三种算法中,第一种和第三种属于在线算法,对大多数类型的题目都有着较好的适应性。但倍增法的查询效率要比RMQ的低,但RMQ虽然好理解,但其实现复杂度比较高,如果是考场上还是用倍增来的稳妥。
而tarjan算法是三种算法中效率最高的,但因其是离线算法所以应用范围不是很广,在面对大量的询问是还是一个不错的算法,且其实现简单,不容易出错。
在考场上的推荐度为RMQ\(\leq\)Tarjan\(\leq\)倍增。