NOIP模板复习(2) LCA的三种解法
NOIP模板复习(2) LCA的三种解法
LCA还是图论中蛮重要的部分,解法众多,这里只拿三个比较常用的板子出来说说
目录
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 <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
vector<int> 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<len;i++)
{
int to=tree[root][i];
if(to==father[root])
{
continue;
}
father[to]=root;
deep[to]=deep[root]+1;
dfs(to);
}
return ;
}
int LCA(int a,int b)
{
if(deep[a]<deep[b])
{
swap(a,b);
}
for(int i=19;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<<a<<" "<<b<<":";
cout<<LCA(a,b)<<endl;
}
return 0;
}
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 <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
vector<int> tree[1005];
vector<int> 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<len;i++)
{
int to=tree[root][i];
if(!used[to])
{
tarjan(to);
unions(root,to);
anc[find(to)]=root;
}
}
len=ask[root].size();
for(int i=0;i<len;i++)
{
int point=ask[root][i];
if(used[point])
{
cout<<root<<" "<<point<<":"<<anc[find(point)]<<endl;//得到最近公共祖先
}
}
}
int main()
{
int n,m,t;
scanf("%d %d %d",&n,&m,&t);
register int a,b;
pre(n);
for(int i=1;i<=m;i++)
{
scanf("%d %d",&a,&b);
tree[a].push_back(b);
}
for(int i=1;i<=t;i++)
{
scanf("%d %d",&a,&b);
ask[a].push_back(b);
ask[b].push_back(a);
}
tarjan(1);
return 0;
}
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 <iostream>
#include <cstdio>
#include <vector>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
vector<int> 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<<j)<=n;j++)
{
for(int i=1;i+(1<<j)-1<=n;i++)
{
int a=st[i][j-1];
int b=st[i+(1<<(j-1))][j-1];
if(deep[a]<=deep[b])
{
st[i][j]=a;
}
else
{
st[i][j]=b;
}
}
}
}
void dfs(int u,int father,int d)//预处理出时间戳
{
id[u]=cnt;
idx[cnt]=u;
deep[cnt++]=d;
int len=tree[u].size();
for(int i=0;i<len;i++)
{
int v=tree[u][i];
if(v==father)
{
continue;
}
dfs(v,u,d+1);
idx[cnt]=u;
deep[cnt++]=d;
}
return ;
}
int ask(int l,int r)
{
int mid=0;
while((1<<(mid+1))<=r-l+1)
{
mid++;
}
int a=st[l][mid];
int b=st[r-(1<<mid)+1][mid];
if(deep[a]<=deep[b])
{
return a;
}
else
{
return b;
}
}
int LCA(int a,int b)
{
int l=id[a];
int r=id[b];
if(l>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<<LCA(a,b)<<endl;
}
return 0;
}
4.总结
上面的三种算法中,第一种和第三种属于在线算法,对大多数类型的题目都有着较好的适应性。但倍增法的查询效率要比RMQ的低,但RMQ虽然好理解,但其实现复杂度比较高,如果是考场上还是用倍增来的稳妥。
而tarjan算法是三种算法中效率最高的,但因其是离线算法所以应用范围不是很广,在面对大量的询问是还是一个不错的算法,且其实现简单,不容易出错。
在考场上的推荐度为RMQ\(\leq\)Tarjan\(\leq\)倍增。