[ABC202E] Count Descendants 题解

题意

有一棵 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;
}
  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值