【洛谷】P7103 族谱树 做题笔记&题解

「C.E.L.U-01」族谱树

题目链接

「C.E.L.U-01」族谱树

题目背景

小 Soup 正在翻看他们家的族谱,他们家的族谱构成了一棵树。小 Soup 发现,由于年代久远,他们家族中的一些分支已经绝迹,他对此十分好奇。

题目描述

小 Soup 给你他们家的族谱树,想要问你在这棵树中所有 k k k 层的孩子(树中深度为 k k k 的点,根节点的深度为 1 1 1 ,根节点编号为 1 1 1 )的 最近公共祖先 \text{最近公共祖先} 最近公共祖先 是谁。

输入格式

第一行两个整数 n , m n,m n,m
第二行 n n n 个整数,其中第 i i i 个整数为 f i f_i fi,表示 i i i 的父亲为 f i f_i fi,请注意, 1 1 1 f i f_i fi 固定为 0 0 0
接下来 m m m 行,每行一个整数 k k k,代表小 Soup 的询问。

输出格式

对于每个小 Soup 的询问,输出一个整数,即所有深度为 k k k 的点的 最近公共祖先 \text{最近公共祖先} 最近公共祖先

样例 #1

样例输入 #1

8 3
0 1 1 2 2 3 4 5
2
1
4

样例输出 #1

1
1
2

样例 #2

样例输入 #2

11 4
0 1 1 3 3 3 4 5 8 8 10
3
4
5
6

样例输出 #2

3
3
8
11

提示

样例解释1:

样例解释2:

数据保证存在深度为 k k k 的点

数据编号 n , m 特殊性质 1 ≤ 10 ╲ 2 ≤ 100 ╲ 3 ∼ 4 ≤ 1 0 3 ╲ 5 ≤ 3 × 1 0 5 树为一条链 6 ≤ 3 × 1 0 5 ╲ 7 ∼ 10 ≤ 3 × 1 0 6 ╲ 11 ∼ 12 ≤ 5 × 1 0 6 ╲ \begin{array}{|c|c|c|}数据编号&n,m&特殊性质\\1&\le10&\diagdown\\2&\le100&\diagdown\\3\sim4&\le10^3&\diagdown\\5&\le3\times10^5&树为一条链\\6&\le3\times10^5&\diagdown\\7\sim10&\le3\times10^6&\diagdown\\11\sim12&\le5\times10^6&\diagdown\end{array} 数据编号1234567101112n,m101001033×1053×1053×1065×106特殊性质树为一条链

对于 100 % 100\% 100% 的数据, n ≤ 5 × 1 0 6 , m ≤ n n\le5\times10^6,m\le n n5×106,mn

温馨提示:此题较卡常,请注意大常数带来的影响以及时空复杂度。如果你被卡常了,可以试试使用快速读入。

思路涉及知识点——Tarjan算法、并查集

本人蒟蒻,浅谈自己的一些看法,如有不对之处,请各位大佬指正。
Tarjan算法是一种离线算法,需要使用并查集记录下每个结点的祖先结点。为了方便遍历每个结点的直接孩子结点,本题我使用的是vector散列表(应该是这个叫法吧)。
首先需要对进行预处理,初始状态,每个并查集数组第 i i i 个结点的祖先是其本身。
然后,使用dfs深搜。过程中,计算出每个结点对应的深度(主要是本题需要回答的询问是每个相同深度结点的LCA)。
遍历当前结点的所有孩子结点,并更新当前结点所在深度的最近祖先结点。若首次找到当前深度,则将祖先结点更新为当前节点本身。再次找到这个深度时,更新祖先结点为并查集数组中记录的当前祖先的祖先结点。
此处有个注意点,更新当前结点的祖先结点应该放在深搜完当前结点之后。这样才可以做到该结点的所有孩子结点的最近祖先结点是当前结点,而不是当前结点的父亲结点。
并查集 find 方法中也需要更新所查 p 结点的最上祖先结点,减少后续查找深度。

题解代码

可能表达的不是很清楚,直接上代码看可能更直观些。由于本题卡常比较严重,此处用到了快读快写,要不然有一个测试点会 TLE

#include<bits/stdc++.h>
using namespace std;
const int N = 5e6 + 3;

int n, m, x;
// f 数组并查集
// deep 数组记录每个结点的深度
// ans 数组记录答案
int f[N], deep[N] = {0}, ans[N];
vector<int> edge[N];

inline int read() // 快读就不用说了吧
{
    int s = 0;
    char c = getchar();
    while (!isdigit(c))
        c = getchar();
    while (isdigit(c))
        s = (s << 3) + (s << 1) + (c ^ 48), c = getchar();
    return s;
}

inline void out(int N){
	if (N < 0){
		putchar('-');
		N = -N;
	}
	if (N >= 10){
		out(N / 10);
	}
	putchar(N % 10 + '0');
}

int find(int p)
{
    return f[p] == p ? p : f[p] = find(f[p]); // 在查找父节点过程更新到最上方父节点
}

void dfs(int y, int fa)
{
    deep[y] = deep[fa] + 1; // 孩子结点的深度在父节点基础上 + 1
    for(int i : edge[y])
    {
        dfs(i, y);
        f[i] = y; // 更新父子结点关系
    }
    ans[deep[y]] = ans[deep[y]] ? find(ans[deep[y]]) : y; 
}

int main()
{
    n = read();
    m = read();
    for(register int i(1); i <= n; ++i)
    {
        f[i] = i; // 初始化并查集
        edge[read()].push_back(i);
    }
    dfs(1, 0);
    while (m--)
    {
        out(ans[read()]);
        putchar('\n');
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值