📜  Tarjan的离线最低共同祖先算法(1)

📅  最后修改于: 2023-12-03 15:05:29.597000             🧑  作者: Mango

离线最低共同祖先算法

离线最低共同祖先(Lowest Common Ancestor,LCA)算法是解决树上问题的重要算法,广泛应用于网络路由、信息检索等领域。Tarjan的离线LCA算法是其中一种经典算法之一,本文将对该算法进行详细介绍。

什么是LCA?

LCA问题指的是,给定一棵有根树和其中的两个节点,找出这两个节点的最深公共祖先。在一棵有根树中,节点x的祖先指的是从根节点到x节点的路径上的所有节点。

Tarjan的离线LCA算法

Tarjan的离线LCA算法是一种预处理算法,可以在线性时间O(N+M)内回答多次LCA查询。假设有多次查询,我们可以将这些查询分为两类:

  1. 在同一路径内的查询,如查询(4, 6)和(3, 5)。
  2. 不在同一路径内的查询,如查询(2, 7)和(4, 5)。

对于第一类查询,显然可以通过预处理每个节点的父节点,在O(1)时间内回答。

对于第二类查询,我们需要一种更加高效的方法。Tarjan算法的基本思路是预处理出每个节点的祖先、处理一些信息并建立数据结构,最后在回答查询时查询数据结构即可。

主要步骤如下:

  1. 对整棵树进行DFS遍历,并记录下每个节点的祖先。

  2. 对于每个查询(u, v),将(u, v)存储到一个数组中,同时用一个数组fa[]记录v节点的父节点。注意,如果已知v的父亲,我们可以将查询改为(u, fa[v])。

  3. 对于节点u,维护一个并查集,将u和其所有的祖先节点都合并到同一个集合中。并查集中存储的是每个节点所属的集合代表元素。

  4. 对于节点v,从v开始沿着其祖先链向上遍历,同时将沿途经过的节点插入到一个set集合中。插入完所有节点后,set集合中保存的是v节点到根节点路径上的所有节点。

  5. 遍历查询数组,对于每个查询(u, v),如果v的父亲fa[v]存在,则将查询修改为(u, fa[v])。

  6. 对于修改后的查询(u, v),从u节点开始沿着其祖先链向上遍历,同时访问集合set[v]。在set[v]中查找第一个属于u节点所在集合的节点,找到即是(u, v)的LCA,否则LCA为根节点。

代码实现

下面是Tarjan的离线LCA算法的C++实现,其中pre[i]表示节点i的父节点,query[i].x和query[i].y表示第i个查询的两个节点。

const int MAXN = 100010, MAXM = 100010;

int n, m, f[MAXN], pre[MAXN], vis[MAXN];
int ans[MAXM], p[MAXN], q[MAXN];

struct Query {int x, y, next;} query[MAXM<<1];

vector<int> G[MAXN];

void init() {
    memset(f, -1, sizeof(f));
    memset(vis, 0, sizeof(vis));
    memset(ans, -1, sizeof(ans));
    memset(pre, -1, sizeof(pre));
    for (int i = 1; i <= n; ++i) G[i].clear();
}

void AddEdge(int u, int v) {
    query[++m].x = u, query[m].y = v, query[m].next = G[u].size(), G[u].push_back(m);
    query[++m].x = v, query[m].y = u, query[m].next = G[v].size(), G[v].push_back(m);
}

int find(int x) {return f[x] == -1 ? x : f[x] = find(f[x]);}

void Union(int u, int v) {if ((u = find(u)) != (v = find(v))) f[v] = u;}

void solve(int u, int fa) {
    pre[u] = fa;
    for (int i = 0; i < G[u].size(); ++i) {
        Query& q = query[G[u][i]];
        if (q.y == fa) continue;
        if (pre[q.y] != -1) {
            ans[G[u][i]/2] = q.x ^ q.y ^ pre[q.y];
        } else {
            solve(q.y, u);
            Union(q.y, u);
        }
    }
    vis[u] = 1;
    for (int i = 0; i < p[u].size(); ++i) {
        int id = p[u][i], v = q[id];
        if (vis[v]) {
            int anc = find(v);
            ans[id] = u ^ v ^ anc;
        }
    }
}

void Tarjan() {
    for (int i = 1; i <= n; ++i)
        if (!vis[i])
            solve(i, -1);
}

以上代码实现的时间复杂度为O(N+Mα(N)),其中α为Ackermann函数。

总结

Tarjan的离线LCA算法是一种高效的解决LCA问题的算法,尤其适用于多次查询的场景。在实现过程中需要注意细节,如查询的分类、并查集的使用以及数组下标的计算等。