题意
有一棵 N N N 个点的有根树,其中 1 1 1 号节点为根,点 i i i 的父亲为 P i P_i Pi。
有 Q Q Q 次查询,每次查询给出两个数 U i U_i Ui 和 D i D_i Di,表示在 U i U_i Ui 的子树中,到根的最短路径刚好经过 D i D_i Di 条边的点有多少个?
思路
这道题,我们可以用利用两次搜索,第一次先将统计每一个点的孩子统计出来,并且记录一下当前这个点到根节点经过了多少个点。而第二次搜索呢,我们记录一下每一个点是在第次搜索搜到的和当前这一次遍历所用的点到根节点经过了多少个点。接着,每一次询问,我们只需要遍历一次就行了,而遍历的起点则是这个点是第几个被搜索到的,终点是起点加上它的孩子的数量减去一。
但是,这样会超时,所以我们可以利用分块去优化它。分块,其实是一种思想,而不是一种数据结构。分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。换到此题,我们可以将原本的进行分块,从而做到询问时最慢只用跑大约 400 400 400 次,满足题意。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAX = 2 * 1e5 + 5;
int n;
int book[MAX];
struct op {//链式前向星
int from, to;
} a[MAX];
int head[MAX];
int cnt = 1;
void hb(int father, int me) {//链式前向星存图
a[cnt].from = head[father];
a[cnt].to = me;
head[father] = cnt++;
}
int sum_son[MAX];
int f[MAX];
inline void dfs(int x) {//第一次搜索
sum_son[x] = 1;//统计子树的节点数量
for (int i = head[x]; i; i = a[i].from) {
int xx = a[i].to;
f[xx] = f[x] + 1;//记录距离根节点的节点数量
dfs(xx);//继续搜索
sum_son[x] += sum_son[xx];//加上子节点子树的节点数量
}
}
int sl[MAX];
int top;
bool tp[MAX];
int dian[MAX];
inline void dfs1(int x) {//第二次搜索
sl[++top] = f[x];//记录当前第top次遍历的点距离根节点的节点数量
dian[x] = top;//记录当前点的dfs序
for (int i = head[x]; i; i = a[i].from) {
int xx = a[i].to;
dfs1(xx);//继续搜索
}
}
int lefta[MAX], righta[MAX];
int dp[505][MAX];
int k;
signed main() {
cin >> n;
int ks = ceil(sqrt(n));//块数
for (int i = 2; i <= n; i++) {
scanf("%d", &k);
hb(k, i);//建图
}
dfs(1);
dfs1(1);
for (int i = 1; i <= n; i++) {
book[i] = i / ks + 1;//第几块
lefta[book[i]] = min(lefta[book[i]], i);//求当前这一块的左端点
righta[book[i]] = max(righta[book[i]], i);//求当前这一块的右端点
}
for (int i = 1; i <= n; i++) {
dp[book[i]][sl[i]]++;//第book[i]块的sl[i]出现的数量增加
}
int m;
cin >> m;
while (m--) {
int x, sum;
scanf("%d%d", &x, &sum);
int ans = 0;
int l_now = dian[x], r_now = dian[x] + sum_son[x] - 1;//左端点和右端点
int b1 = book[l_now], b2 = book[r_now];//左右两段点所在的分别是第几块
if (b1 == b2) {//在同一块
for (int j = l_now; j <= r_now; j++) {//直接遍历
ans += (sl[j] == sum);
}
printf("%d\n", ans);
continue;
}
for (int j = b1 + 1; j <= b2 - 1; j++) {//加上左右两块中的数量
ans += dp[j][sum];
}
int lb1 = lefta[b1], rb1 = righta[b1];//记录左边那一块的左端点和右端点
int midb1 = (lb1 + rb1) >> 1;//求中间点
if (l_now >= midb1) {//要是大于,就直接遍历
for (int j = l_now; book[j] == b1; j++) {
if (sl[j] == sum) {
ans++;
}
}
} else {//否则减去相反的一边,再加上这一块的
int l = 0;
for (int j = l_now - 1; book[j] == b1; j--) {
if (sl[j] == sum) {
l++;
}
}
ans += dp[b1][sum] - l;
}
int lb2 = lefta[b2], rb2 = righta[b2];//记录右边那一块的左端点和右端点
int midb2 = (lb2 + rb2) >> 1;//求中间点
if (r_now >= midb2) {//要是大于就减去相反的一边,再加上这一块的
int l = 0;
for (int j = r_now + 1; book[j] == b2; j++) {
if (sl[j] == sum) {
l++;
}
}
ans += dp[b2][sum] - l;
} else {//否则就直接遍历
for (int j = r_now; book[j] == b2; j--) {
if (sl[j] == sum) {
ans++;
}
}
}
printf("%d\n", ans);
}
return 0;
}