LCA(离线算法)

离线查询与有向树的LCA应用
本文介绍了一种使用LCA离线算法解决有向树中查询问题的方法,具体步骤包括构建树结构、查找祖先节点、实现离线查询及结果统计。
#include<iostream>
#include<cstdio>
#include<string.h>
#include<cstring>
#include<string>
#include<stack>
#include<set>
#include<algorithm>
#include<cmath>
#include<vector>
#include<map>

#define LOCAL
#define ll long long
#define lll unsigned long long
#define MAX 1000009
#define eps 1e-8
#define INF 0x7fffffff
#define mod 1000000007

using namespace std;
/*

题意:给出一颗有向树,Q个查询,输出查询点的个数

想法:LCA离线算法-Tarjan,模板题

*/
const int maxn = 1010;
const int Maxn = 500010;//查询数的最大值

int father[maxn];
int Find(int x)
{
    return father[x]==-1?x:Find(father[x]);
}
void Union(int x,int y)
{
    int xx = Find(x);
    int yy = Find(y);
    if(xx==yy) return;
    father[xx] = yy;
}
bool vis[maxn*2];
int ancestor[maxn];//祖先
struct Edge
{
    int to;
    int next;
} edge[maxn*2];
int head[maxn],tot;
void addedge(int u,int v)
{
    edge[tot].to = v;
    edge[tot].next = head[u];
    head[u] = tot++;
}
struct Query
{
    int q,next;
    int index;//查询编号
} query[Maxn*2];
int answer[Maxn];//存储最后查询的结果,下角标0~n -1
int h[Maxn];
int tt;
int Q;

void add_query(int u,int v,int index)
{
    query[tt].q = v;
    query[tt].next = h[u];
    query[tt].index = index;
    h[u] = tt++;
    query[tt].q = u;
    query[tt].next = h[v];
    query[tt].index = index;
    h[v] = tt++;
}

void init()
{
    tot = 0;
    memset(head,-1,sizeof(head));
    tt = 0;
    memset(h,-1,sizeof(h));
    memset(vis,false,sizeof(vis));
    memset(father,-1,sizeof(father));
    memset(ancestor,0,sizeof(ancestor));
}
void LCA(int u)
{
    ancestor[u] = u;
    vis[u] = true;
    for(int i = head[u]; i != -1; i = edge[i].next)//与u相连接的点
    {
        int v = edge[i].to;
        if(vis[v])continue;
        LCA(v);//处理子树
        Union(u,v);//然后链接到父亲
        ancestor[Find(u)] = u;//子树的所有祖先都是u
    }
    for(int i = h[u]; i != -1; i = query[i].next)
    {
        int v = query[i].q;
        if(vis[v])
        {
            answer[query[i].index] = ancestor[Find(v)];//按顺序询问,得出离线查询的顺序
        }
    }
}
bool flag[maxn];
int Count_num[maxn];

int main()
{
    //freopen("test.in","r",stdin);
    int n;
    int u,v,k;
    while(~scanf("%d",&n))
    {
        init();
        memset(flag,false,sizeof(flag));
        for(int i = 1; i<=n; i++)
        {
            scanf("%d:(%d)",&u,&k);
            while(k--)
            {
                scanf("%d",&v);
                flag[v] = true;
                addedge(u,v);
                addedge(v,u);
            }
        }
        scanf("%d",&Q);
        for(int i = 0; i<Q; i++)
        {
            char ch;
            cin>>ch;
            scanf("%d %d)",&u,&v);
            add_query(u,v,i);
        }
        int root;
        for(int i = 1; i<=n; i++)
        {
            if(!flag[i])//查找一个没有出现过得点作为初始根节点
            {
                root = i;
                break;
            }
        }
        LCA(root);
        memset(Count_num,0,sizeof(Count_num));
        for(int i = 0; i<Q; i++)
        {
            Count_num[answer[i]] ++;
        }
        for(int i = 1; i<=n; i++)
        {
            if(Count_num[i]>0)
            {
                printf("%d:%d\n",i,Count_num[i]);
            }
        }
    }
    return 0;
}

<think>嗯,我现在需要了解LCA算法。首先,LCA是什么的缩写呢?好像是在计算机科学或者图论里的术语。对了,LCA应该是最低公共祖先(Lowest Common Ancestor)的缩写吧。对的,之前听说过这个算法,但具体细节不太清楚。让我仔细想想,它的应用场景是什么?比如在树结构中找到两个节点的最近公共祖先节点,对吧?比如在二叉树中,找到节点p和q的最近共同祖先。 那这个算法有什么常见的实现方法呢?记得有暴力法、递归法,还有Tarjan算法和二进制跳转法什么的。不过可能需要更详细的了解。先从暴力法开始,暴力法的思路应该是分别记录从根节点到两个节点的路径,然后比较这两条路径,找到最后一个相同的节点。这样时间复杂度可能比较高,尤其是当树很大的时候,因为每次查询都需要遍历路径。 然后是递归法,可能是在二叉树中通过递归查找左右子树是否存在目标节点,然后判断LCA的位置。比如在二叉搜索树中,可以利用节点值的大小来简化查找过程。不过对于普通二叉树,可能需要后序遍历的方式,从下往上找。 Tarjan算法好像是离线算法,可以处理多个查询,利用并查集结构来优化时间复杂度。不过具体步骤可能有点复杂,需要深度优先搜索和并查集的结合。 还有二进制跳转法,也就是倍增法,预处理每个节点的不同距离的祖先,比如2^k层祖先,这样在查询时可以通过跳转来快速找到LCA。这种方法的时间复杂度预处理是O(n log n),查询是O(log n),适合多次查询的情况。 需要更详细地理清楚每个方法的步骤和优缺点。比如递归法的时间复杂度是O(n),每个节点访问一次,而倍增法需要预处理,但查询更快。Tarjan算法在处理多个查询时效率更高,因为是离线处理,所有查询一次性处理完毕。 举个例子,假设有一个二叉树,节点结构包含左右子节点指针。如何用递归方法找到LCA?可能递归函数返回的是当前子树中是否包含p或q,或者LCA本身。当左右子树各包含一个节点时,当前根节点就是LCA。如果左子树返回非空,右子树也返回非空,那么当前节点就是LCA。如果只有一边非空,则返回那边的结果。 对于Tarjan算法,可能需要记录访问状态,当访问完一个节点的所有子节点后,将其与父节点合并,同时在处理查询时,如果另一个节点已经被访问过,就可以通过并查集找到它们的LCA。 而倍增法的步骤大致是预处理每个节点的祖先表,然后通过将较深的节点向上跳转到与另一个节点同一深度,再同时向上跳转直到找到公共祖先。 可能还需要考虑树的结构,比如是否有父指针,或者是否是二叉树、多叉树等不同情况。不同的数据结构可能需要不同的处理方式。 另外,关于LCA的应用,比如在计算树中两个节点之间的距离时,可以先找到LCA,然后计算各自到LCA的距离之和。或者在语法分析、版本控制系统中的最近共同祖先等场景中的应用。 总结来说,LCA算法的主要方法包括暴力法、递归法、Tarjan的离线算法以及倍增法,各有不同的时间复杂度和适用场景。需要根据具体的问题需求选择合适的算法实现。</think>LCA(Lowest Common Ancestor,最低公共祖先)算法用于在树或图中找到两个节点的最近公共祖先节点。以下是其核心介绍: --- ### 一、基本概念 - **定义**:对于树中的两个节点 $u$ 和 $v$,它们的 LCA 是深度最大的节点 $w$,且 $w$ 同时是 $u$ 和 $v$ 的祖先。 - **示例**: $$ \text{若树的结构为 } root \to A \to B \to C \text{ 和 } root \to A \to D, $$ 则 $C$ 和 $D$ 的 LCA 是 $A$。 --- ### 二、常见算法及实现 #### 1. 暴力法(路径比较) - **步骤**: 1. 分别找到从根节点到 $u$ 和 $v$ 的路径。 2. 比较两条路径,最后一个相同的节点即为 LCA。 - **时间复杂度**:$O(n)$ 每次查询($n$ 为节点数)。 #### 2. 递归法(适用于二叉树) - **原理**:通过递归遍历,若当前子树同时包含 $u$ 和 $v$,则当前根节点为 LCA。 - **代码逻辑**: ```python def lca(root, p, q): if not root or root == p or root == q: return root left = lca(root.left, p, q) right = lca(root.right, p, q) if left and right: return root # 当前根是 LCA return left if left else right ``` - **时间复杂度**:$O(n)$。 #### 3. Tarjan 算法离线算法,多查询优化) - **步骤**: 1. 通过 DFS 遍历树,利用并查集记录已访问节点。 2. 对每个查询 $(u, v)$,若 $v$ 已被访问,则通过并查集找到 LCA。 - **时间复杂度**:$O(n + q)$(预处理 $n$ 个节点,处理 $q$ 次查询)。 #### 4. 倍增法(二进制跳转) - **原理**:预处理每个节点的 $2^k$ 级祖先,通过跳转快速对齐深度并查找 LCA。 - **步骤**: 1. 预处理:为每个节点构建祖先表(如 $ancestor[i][k]$ 表示节点 $i$ 的 $2^k$ 级祖先)。 2. 查询时: - 将较深的节点跳转到与另一个节点同深度。 - 同时向上跳转,直到找到 LCA。 - **时间复杂度**:预处理 $O(n \log n)$,单次查询 $O(\log n)$。 --- ### 三、应用场景 1. **计算树中节点距离**:若 $u$ 到 $v$ 的距离为 $depth(u) + depth(v) - 2 \times depth(LCA(u, v))$。 2. **版本控制系统**:寻找文件修改的最近共同祖先版本。 3. **语法分析**:在语法树中定位公共结构。 --- ### 四、算法选择建议 - **单次查询**:递归法或暴力法。 - **多次查询**:倍增法或 Tarjan 算法。 - **二叉树结构**:优先递归法;普通树或图可用路径比较或倍增法。 通过结合具体场景和数据结构特点,选择高效实现方式即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值