图论--LCA--Tarjan(离线)

本文介绍了一种针对有向树结构的查询优化算法,能够高效处理大量查询,输出每个节点的出现次数,复杂度达到O(n+Q),适用于大规模数据处理场景。
*
 *  给出一颗有向树,Q个查询
 *  输出查询结果中每个点出现次数
 *  复杂度O(n + Q);
 */
const int MAXN = 1010;
const int MAXQ = 500010;    //  查询数的最大值

//  并查集部分
int F[MAXN];                //  需要初始化为-1

int find(int x)
{
    if (F[x] == -1)
    {
        return x;
    }
    return F[x] = find(F[x]);
}

void bing(int u, int v)
{
    int t1 = find(u);
    int t2 = find(v);
    if (t1 != t2)
    {
        F[t1] = t2;
    }
}

bool vis[MAXN];             //  访问标记
int ancestor[MAXN];         //  祖先
struct Edge
{
    int to, 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[MAXQ * 2];

int answer[MAXQ];           //  存储最后的查询结果,下标0~Q-1
int h[MAXQ];
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(F, -1, sizeof(F));
    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)
    {
        int v = edge[i].to;
        if (vis[v])
        {
            continue;
        }
        LCA(v);
        bing(u, v);
        ancestor[find(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()
{
    int n;
    int u, v, k;
    while (scanf("%d", &n) == 1)
    {
        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> 我们先理解用户的问题:用户想查找P5002专心OI题目中使用tarjan算法找祖先的相关内容。 根据引用内容,我们了解到Tarjan算法通常用于强连通分量的查找,但这里用户提到“找祖先”,这通常与最近公共祖先(LCA)问题相关。实际上,Tarjan也提出了一个离线LCA的算法(即Tarjan-LCA算法)。 因此,我们推断用户可能在题目P5002中需要用到Tarjan算法来求解最近公共祖先(LCA)问题。 现在,我们结合引用内容和Tarjan算法在LCA问题上的应用来回答。 ### Tarjan算法求最近公共祖先(LCATarjan算法可以离线解决最近公共祖先问题。它的基本思想是深度优先遍历整个树,并利用并查集来高效地回答一系列的查询。 #### 算法步骤: 1. 初始化:将所有节点的父节点设为自身,并设置访问标记为未访问。 2. 从根节点开始深度优先遍历。 3. 对于当前节点u: - 标记u为已访问。 - 递归遍历u的所有子节点v,并在递归返回时将v所在集合与u合并(通过并查集)。 4. 处理所有与u相关的查询:对于查询(u, v),如果v已经被访问过,那么v当前所在集合的代表元素(并查集的根)就是u和v的最近公共祖先。 #### 证明(参考引用[2]的思想): 在深度优先遍历过程中,当我们访问到u时,如果v已经被访问过,那么v所在集合的代表元素(即v向上回溯到当前还未回溯完的路径上的一个祖先)就是u和v的最近公共祖先。这是因为并查集的合并是在递归返回时进行的,所以当前集合的代表元素就是深度优先遍历过程中当前子树的最顶端节点,也就是u和v的公共祖先中深度最小的那个。 #### 代码示例(C++风格伪代码): ```cpp vector<int> G[N]; // 图的邻接表 vector<pair<int, int>> queries[N]; // 存储查询:queries[u] = {v, 查询编号} int ans[Q]; // 存储每个查询的答案 int parent[N]; // 并查集的父节点 int ancestor[N]; // 当前节点的祖先(在并查集中) bool visited[N]; // 访问标记 // 并查集的查找 int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } // 并查集的合并(这里直接路径压缩,合并时注意方向) void merge(int x, int y) { // 将y合并到x的集合中,通常我们合并时是将子节点合并到父节点 int rx = find(x), ry = find(y); if (rx != ry) { parent[ry] = rx; } } // Tarjan-LCA算法(离线) void tarjan(int u, int father) { visited[u] = true; ancestor[u] = u; // 初始化自身为集合代表 for (int v : G[u]) { if (v == father) continue; tarjan(v, u); merge(u, v); // 将v的集合合并到u的集合 ancestor[find(u)] = u; // 合并后,u所在集合的代表更新为u(其实在merge中已经通过路径压缩,这里可省略,但为了清晰,可以这样理解) } // 处理查询 for (auto [v, idx] : queries[u]) { if (visited[v]) { ans[idx] = ancestor[find(v)]; // 或者直接find(v)就是v所在集合的代表,即LCA } } } ``` 在实际实现中,我们通常会将祖先信息记录在并查集的根节点上,或者利用并查集的查找直接得到祖先。注意,上面代码中的`ancestor`数组实际上可以省略,因为我们可以将祖先信息直接记录在并查集的根节点上。下面给出更常见的写法: ```cpp int parent[N]; // 并查集父节点 int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } void tarjan(int u, int fa) { parent[u] = u; // 初始化自成一个集合 for (int v : G[u]) { if (v == fa) continue; tarjan(v, u); parent[v] = u; // 将v的父节点指向u,合并集合(实际上是把v的集合合并到u) } vis[u] = true; // 处理所有与u相关的查询 for (auto q : query[u]) { int v = q.first, id = q.second; if (vis[v]) { ans[id] = find(v); // u和v的LCA就是v所在集合的代表 } } } ``` 注意:这里并查集在合并时,是将子节点的集合合并到父节点。在递归返回后,父节点的集合代表就是父节点本身(因为合并后,父节点的父节点还是自己,而子节点的父节点指向了父节点,所以通过路径压缩后,子节点的代表也是父节点)。当处理查询时,如果另一个节点已经被访问过,那么通过find操作就可以找到另一个节点所在集合的代表,即最近公共祖先。 #### 在题目P5002专心OI中的应用 由于题目P5002的具体内容我们没有,但根据题目名称“专心OI”和用户提到的“找祖先”,我们推测该题需要求解最近公共祖先。具体在题目中的应用可能是: - 求解树上两点间的距离(通过LCA- 求解树上路径上的某些权值等 因此,使用Tarjan算法可以高效地离线处理所有查询。 ### 相关问题 §§相关问题§§ 1. Tarjan算法求LCA的时间复杂度是多少? 2. 为什么Tarjan算法求LCA离线的?有没有在线的算法? 3. 如何用Tarjan算法求树上两点间的最短路径? 4. 在题目P5002中具体是如何应用Tarjan算法的? </think> ### P5002《专心OI》题解:Tarjan算法在找祖先中的应用 在题目P5002中,Tarjan算法主要用于解决**最近公共祖先(LCA)问题**,这是图论中的经典应用。以下是核心原理和实现要点: #### 1. **算法目标** - 给定一棵树和若干查询 `(u, v)`,快速找到两点最近的公共祖先。 - Tarjan算法通过**深度优先遍历(DFS)** 和 **并查集** 实现离线查询(所有查询预先已知)。 #### 2. **关键步骤** - **DFS遍历**:从根节点开始递归遍历子树。 - **并查集合并**:DFS回溯时,将子节点集合合并到父节点集合。 - **查询处理**:当遍历到节点 `u` 时,若查询的另一节点 `v` 已被访问,则 `v` 的当前集合代表即为LCA。 #### 3. **代码实现框架(C++)** ```cpp vector<int> G[N]; // 树的邻接表 vector<pair<int, int>> Q[N]; // 查询:Q[u] = {v, 查询编号} int parent[N], ans[QID]; // 并查集父节点、答案存储 bool vis[N]; // 访问标记 int find(int x) { return parent[x] == x ? x : parent[x] = find(parent[x]); } void tarjan(int u, int fa) { parent[u] = u; // 初始化自环 for (int v : G[u]) { if (v == fa) continue; tarjan(v, u); parent[v] = u; // 回溯时合并子节点集合到当前节点 } vis[u] = true; // 标记已访问 for (auto [v, qid] : Q[u]) { // 处理与u相关的查询 if (vis[v]) ans[qid] = find(v); // LCA为v的集合代表 } } ``` #### 4. **算法原理图解** ``` 树结构示例: 1 / \ 2 3 / \ 4 5 查询 (4,5)LCA- DFS遍历到4时,标记vis[4]=true - 回溯到2,合并4→2 - 遍历到5时,查询4已被访问,find(4)=2 → LCA(4,5)=2 ``` (参考引用[2]的DFS树合并机制[^2]) #### 5. **题目P5002的特殊性** - **应用场景**:题目可能涉及树上路径统计(如两点间距离 $dist(u,v) = depth[u] + depth[v] - 2 \times depth[LCA(u,v)]$)。 - **性能优势**:Tarjan算法时间复杂度 $O(n + q \alpha(n))$,适合大规模离线查询($\alpha$为并查集反阿克曼函数)。 > **注意**:若题目要求在线查询(查询动态到达),需改用倍增法或树链剖分(参考引用[3]的扩展建议[^3])。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值