给定一棵 n 个节点的树。
节点的编号为 1∼n,其中 1 号节点为根节点,每个节点的编号都大于其父节点的编号。
现在,你需要回答 q 个询问。
每个询问给定两个整数 ui,ki。
我们希望你用 DFS(深度优先搜索)算法来遍历根节点为 ui 的子树。
我们规定,当遍历(或回溯)到某一节点时,下一个遍历的目标应该是它的未经遍历的子节点中编号最小的那一个子节点。
例如,上图实例中:
如果遍历根节点为 1 号节点的子树,则子树内各节点的遍历顺序为 [1,2,3,5,6,8,7,9,4]。
如果遍历根节点为 3 号节点的子树,则子树内各节点的遍历顺序为 [3,5,6,8,7,9]。
如果遍历根节点为 7 号节点的子树,则子树内各节点的遍历顺序为 [7,9]。
如果遍历根节点为 9 号节点的子树,则子树内各节点的遍历顺序为 [9]。
每个询问就是让你计算采用规定的 DFS 算法来遍历根节点为 ui 的子树时,第 ki 个被遍历到的节点的编号。
输入格式
第一行包含两个整数 n , q n,q n,q。
第二行包含 n − 1 n−1 n−1 个整数 p 2 , p 3 , … , p n , p_2,p_3,…,p_n, p2,p3,…,pn,其中 p i p_i pi 表示第 i i i 号节点的父节点的编号。
接下来 q q q 行,每行包含两个整数 u i u_i ui, k i k_i ki,表示一组询问。
输出格式
共 q q q 行,每组询问输出一行一个整数表示第 k i k_i ki 个被遍历到的节点的编号。
如果第 k i k_i ki 个被遍历到的节点不存在,则输出 −1。
数据范围
前三个测试点满足 2 ≤ n ≤ 20 2≤n≤20 2≤n≤20, 1 ≤ q ≤ 20 1≤q≤20 1≤q≤20。
所有测试点满足 2 ≤ n ≤ 2 × 1 0 5 2≤n≤2×10^5 2≤n≤2×105, 1 ≤ q ≤ 2 × 1 0 5 1≤q≤2×10^5 1≤q≤2×105, 1 ≤ p i < i 1≤p_i<i 1≤pi<i, 1 ≤ u i , k i ≤ n 1≤u_i,k_i≤n 1≤ui,ki≤n。
输入样例:
9 6
1 1 1 3 5 3 5 7
3 1
1 5
3 4
7 3
1 8
1 9
输出样例:
3
6
8
-1
9
4
思路分析
- 最初的思考:每一次询问,dfs遍历,在dfs的过程中找到第k个数
- 但是,这样做的问题在于a)复杂度太高,每一次询问都要dfs;b)如果遍历数值更小的子节点?
- 优化:思考能不能就用一次dfs遍历所有路径,就能应对q次询问呢?——答案是可以的,但是需要考虑以下几个问题
- 要按照子节点编号顺序升序遍历?
- 利用vector h[],邻接表数组存储,每一个数组元素内部包含所有i节点的子节点,调用sort()即可实现子节点升序排列
- 针对u节点,如何快速定位到该节点的路径?
- 利用额外数组,记录每个节点u在路径中的定位
- 针对u节点,如何才能直到是否无解?
- 在dfs遍历的时候,需要记录每个节点的子节点个数(包含其自身)【因为样例中k=1时就是其本身节点u】
- 要按照子节点编号顺序升序遍历?
实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 200010;
vector<int> h[N]; // 临街表存储,将树看成有向图,用vector后续节点排序
int cnt[N], idx[N]; // idx作用是记录路径上节点u的idx
int n, q;
vector<int> path;
// 添加a->b
void add(int a, int b)
{
h[a].push_back(b);
}
// 返回以u为根结点的子节点的数量
int dfs(int u)
{
path.push_back(u);
cnt[u] = 1;
for(int i = 0; i < h[u].size(); i++){
cnt[u] += dfs(h[u][i]);
}
return cnt[u]; //本身加上其子节点数量
}
int main()
{
scanf("%d%d", &n, &q);
for(int i = 2; i <= n; i++){
int fa;
scanf("%d", &fa);
add(fa, i); // 从father节点指向 i
}
for(int i = 1; i <= n; i++){
sort(h[i].begin(), h[i].end());
}
dfs(1);
for(int i = 0; i < n; i++) idx[path[i]] = i; //节点到路径的快速映射
while(q--){
int u, k;
scanf("%d%d", &u, &k);
if(k > cnt[u]) puts("-1");
else printf("%d\n", path[idx[u] + k - 1]);
}
return 0;
}
一个小细节:如何实现记录某一个树节点其子节点的个数?
// 实现1: 返回以u为根结点的子节点的数量(包含自身)
int dfs(int u)
{
path.push_back(u);
cnt[u] = 1;
for(int i = 0; i < h[u].size(); i++){
cnt[u] += dfs(h[u][i]);
}
return cnt[u]; //本身加上其子节点数量
}
// 实现2: 返回以u为根结点的子节点的数量(不包含自身)
int dfs(int u)
{
path.push_back(u);
for(int i = 0; i < h[u].size(); i++){
cnt[u] += dfs(h[u][i]);
}
return 1 + cnt[u]; //作为子节点,向上层父节点返回自其自身和子节点的数量
}
仔细观察上面两个版本,对于子节点数量计算的结果有何不同之处?
- 最简单情况,从仅有一个节点考虑
- 当仅有一个节点,第一种返回1;第二种返回0
- 实现1中根节点将1(其自身)记录进子节点数组中了,而实现2虽然也是1+cnt,但是1并没有被记录进子节点数组
- 再看有三个节点时的情况