您的位置:首页 > 其它

LCA-Least Common Ancestors(最近公共祖先)

2018-03-26 22:38 405 查看

问题类型

给定一棵有根多叉树,给出指定的两个结点的最近公共祖先

求解方法

倍增

Tarjan(并查集+DFS)

RMQ(Range Minimum Query,区间最小值查询+时间戳

倍增法

用倍增优化之前的代码:

struct edge//链式前向星
{
int to;
int next;
};
edge e[1000010];
int head[500010];
int cnt = 1;
int n,m,s;

int deepth[500010];//结点的深度
int father[500010];//父结点
bool book[500010];//标记数组
void add_edge(int u,int v)
{
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
cnt++;
}
void dfs(int st,int step)//当前结点,树的深度
{
book[st] = true;
deepth[st] = step;
for(int i = head[st]; i != -1; i = e[i].next)
{
int to = e[i].to;
if(book[to] == false)//如果没被标记
{
father[to] = st;//更新父亲
dfs(to,step + 1);//继续搜索
}
}
return;
}
int Lca(int a,int b)
{
if(deepth[a] < deepth)//这样就可以只写一个while循环
swap(a,b);
while(deepth[a] > deepth[b])
a = father[a];
while(a != b)
{
//不相等就向上移动
a = father[a];
b = father[b];
}
return a;//返回的是a,不是father[a]
}
int main()
{
cin >> n >> m >> s;//结点数、询问数、根结点
memset(head,-1,sizeof(head));//初始化
for(int i = 1; i <= n - 1; ++i)//建树
{
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
father[s] = s;//初始化根的父亲为它自己
dfs(s,1);//搜索,求出深度和父亲
for(int i = 1; i <= m; ++i)//查询
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",Lca(a,b));
}
return 0;
}


[b]倍增代码:


struct edge//链式前向星
{
int to;
int next;
};
edge e[1000010];
int head[500010];
int cnt = 1;
int n,m,s;

int book[500010];
int deepth[500010];
int dp[500010][25];//dp[i][j]表示结点i向上跳2^j步所到达的结点
void add_edge(int u,int v)//建边
{
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
cnt++;
}
void dfs(int st,int step)//当前结点,树的深度
{
book[st] = true;//标记
deepth[st] = step;
for(int i = head[st]; i != -1; i = e[i].next)
{
int to = e[i].to;
if(book[to] == false)//如果没被标记
{
dp[to][0] = st;//更新父亲
dfs(to,step + 1);//继续搜索
}
}
return;
}
void calc_dp()
{
int max_step = log(n) / log(2);//最大的一步可以跳多少。考虑树是一条直链时的极限情况,这时候最深的结点最大的一步能向上跳{2的[log2(n)取下整]次方}步。我们的步子是2的幂次,所以要这样取值
for(int i = 1; i <= max_step; ++i)//一个步子的范围为[1,n],即[2^0,2^log2(n)],又因为dp[][0]已被求出,所以i从1开始
{
for(int j = 1; j <= n; ++j)//对于结点j
{
if(dp[j][i - 1] != -1)//如果没越界,很重要!
dp[j][i] = dp[dp[j][i - 1]][i - 1];
}
}
return;
}
int Lca(int a,int b)
{
if(deepth[a] < deepth)//令a为较深的结点
swap(a,b);
int max_s = log(deepth[a]) / log(2);//结点a能跳的最大的一步为2^max_s
for(int i = max_s; i >= 0; --i)//从最大的一步开始跳,循环结束后,a和b必位于同一层
if(deepth[a] - pow(2,i) >= deepth[b])//如果跳完了深度依然大于等于b,就跳。
a = dp[a][i];
if(a == b)//如果相等,说明当前结点就是最近公共祖先
return a;
for(int i = max_s; i >= 0; --i)
{
//跳到最近公共祖先的子节点处
if(dp[a][i] != -1 && dp[a][i] != dp[b][i])//一起向上跳,如果没有越界且跳完后的结点不相同,就跳。相等就不跳了
{
a = dp[a][i];
b = dp[b][i];
}
}
return dp[a][0];
}
int main()
{
cin >> n >> m >> s;//结点数、询问数、根结点
memset(head,-1,sizeof(head));//初始化
for(int i = 1; i <= n - 1; ++i)//建树
{
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
memset(dp,-1,sizeof(dp));//初始化
dp[s][0] = s;//将根结点的父亲初始化为它本身
dfs(s,1);//求出每个结点的深度以及dp的边界值(父亲)
calc_dp();//计算
for(int i = 1; i <= m; ++i)
{
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",Lca(a,b));
}
return 0;
}


[b]解决方法:


在树中,除了根结点,每一个结点有且只有一个父亲,我们可以记录结点i的父亲为结点father[i],这样我们就可以根据父亲结点向上移动,直到遇到相同的父亲结点,即为最近公共祖先。考虑到结点a和结点b很有可能不在同一深度上,于是用deepth数组记录结点在树中的深度,深度大的主动移动到深度小的结点的同一深度水平上,再进行向上移动。显然,对树的遍历可以用搜索解决。

以上未用倍增优化,超时。

如果只是用一次的话,一般的LCA也就够了,它的时间复杂度为O(n) ,而倍增优化的Lca的时间复杂度为O(logn)

若干结论:

a与b的最近公共祖先一定位于同一深度上(显然)

任何一个数都可以表示成2的幂次之和的形式

dp[i][j]
用递推来求出,因为其子问题
dp[i][j - 1]
已被求出,而
dp[i][j] = dp[[i][j - 1]][j - 1]


到达了同一结点后,不论再怎么向上走,到达的显然还是同一结点

RMQ+时间戳

代码:

这里写代码片


解决方法:

对于涉及有根树的问题,将树转为从根DFS标号后得到的序列处理的技巧常常十分有效
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: