LCA
0.3
目录
基本概念
LCA是求最近公共祖先问题,有在线st/RMQ和离线Tarjan/并查集两种算法,以及倍增的特殊做法 (倍增是一种思想). 虽然倍增的做法更常见,但是两种都要遍历一次树,直接处理问题的Tarjan在时间上更好.选择更好的算法,而不是更多的常数优化应该是编程的好习惯 (仅供参考) .
Tarjan算法只需要遍历一次树 (一边遍历一边查询,所以要先存储询问) ,所以其时间复杂度是O(n+m (询问个数) ).
Tarjan
大致思路
- 从根节点出发开始搜索
- 找到所有子节点并继续搜索
- 在搜索过程中记录每个访问的点的lca
实现过程
int find(int i)
{
if(i!=f[i]) f[i]=find(f[i]);
return f[i];
}
void lca(int u)
{
v[u]=true;//标记以访问过
for(int i=first[u];i;i=a[i].ne)//链式前向星在遍历时比较方便且省时间
{
int e=a[i].t;
if(v[e]) continue;
//因为不清楚父子关系,为避免环,需要加一个标记
lca(e);//拓展e和它的子节点,同时遍历
f[e]=u;//将e并入u
}
for(int i=firs[u];i;i=b[i].ne)
{
int e=b[i].t;
if(!v[e]) continue;//还没有访问到的点的父亲是它自己
if(i&1)
//直接将答案与询问边一起存储,注意输出时要把i<<1 (双向存储询问)
b[i+1].ans=b[i].ans=find(e);
else
b[i-1].ans=b[i].ans=find(e);
}
}
倍增
基本思路
倍增是一种快速的搜索方式,可以在较短的时间找到目标,但是因为是在线算法,在时间开销上可能比Tarjan大.简单来说,倍增就是跳跃式前进,以计算机习惯的运动方式 (二进制).预处理为O(nlogn),查询为常数级 (最多只需要到31) ,基本看成O(1).在询问数较大时可能会快一点 (但是因为不管是哪种算法都需要极大的空间开销,所以一般不需要作此类考虑)
实现过程
void dfs(int u){
d[u]=d[f[u][0]]+1;//子节点比父节点深度多1
for(int i=0;f[u][i];i++)
f[u][i+1]=f[f[u][i]][i];
//可以结合关系并查集理解,简单的关系转换
for(int i=first[u];i;i=a[i].ne)
if(!d[a[i].t])//如果没有处理过
{
f[a[i].t][0]=u;//f[e]=u
dfs(a[i].t);//继续拓展
}
}
int lca(int u,int v){
if(d[u]>d[v])//位运算交换
u^=v^=u^=v;
for(int i=bb;i>=0;i--)
if(d[f[v][i]]>=d[u])
v=f[v][i];
if(u==v)
return v;
for(int i=bb;i>=0;i--)
if(f[v][i]!=f[u][i])
{
v=f[v][i];
u=f[u][i];
}
return f[u][0];//直接输出即可
}
int dete(unsigned x)//不用unsigned就RE,因为数字非常大 (十进制下)
//bb=dete(n),用来判断位数,小优化.在卡常时效果会更好
{
int n=1;
if(x==0) return -1;
if ((x>>16) == 0) {n = n+16; x = x<<16;}
if ((x>>24) == 0) {n = n+8; x = x<<8;}
if ((x>>28) == 0) {n = n+4; x = x<<4;}
if ((x>>30) == 0) {n = n+2; x = x<<2;}
n = n-(x>>31);
return 31-n;
}