acm暑校第7题——Apple Tree

1.题目

描述

卡卡的房子外面有一棵苹果树。每年秋天,树上都会长出很多苹果。卡卡非常喜欢苹果,所以他一直在小心翼翼地培育这棵大苹果树。

这棵树有 N个叉子,这些叉子由树枝连接。Kaka 用 1 到 N对叉子进行编号,根始终用 1 编号。苹果会长在叉子上,两个苹果不会长在同一根叉子上。卡卡想知道一棵子树中有多少个苹果,用于研究苹果树的生产能力。

麻烦的是,有一段时间,一个新苹果可能会在空叉子上长出来,而卡卡可能会从树上摘下一个苹果作为他的甜点。你能帮忙卡卡吗?

输入

第一行包含一个整数 N (N ≤ 100,000) ,这是树中分叉的数量。
以下 N - 1 行各包含两个整数 u 和 v,这意味着 fork u 和 fork v 通过一个分支连接。
下一行包含整数 M (M ≤ 100,000)。
接下来的 M 行每行都包含一条消息,该消息是
x”,这意味着叉子 x 上的苹果的存在已更改。即如果叉子上有一个苹果,那么卡卡就摘它;否则,空叉子上长出了一个新的苹果。

x”,表示查询叉子 x 上方子树中的苹果数量,包括叉子 x
上的苹果(如果存在) 注意树开始时装满了苹果

输出

对于每个查询,每行输出相应的答案。

样例输入

3
1 2
1 3
3
Q 1
C 2
Q 1

样例输出

3
2

2.代码

  • 树的表示:

    • 使用邻接表表示树结构。
    • 每个节点的边被存储在 a 数组中,hd 数组保存每个节点的边的起始位置。
  • 树状数组(BIT)的使用:

    • 树状数组的作用:用于支持前缀和查询以及单点更新,这里用于维护和查询每个节点的访问状态。
    • c 数组用于树状数组,change() 函数用于更新操作,ask() 函数用于查询操作。
  • DFS遍历与时间戳:

    • DFS遍历:从树的根节点开始,遍历所有节点,并记录每个节点的起始时间戳 st[]结束时间戳 en[]
    • 时间戳:每个节点的子树可以通过时间戳范围来唯一标识。即子树中所有节点的时间戳都在该节点的起始和结束时间戳之间。
  • 节点标记与查询:

    • 节点标记:通过树状数组 c 维护每个节点是否已访问的状态。初始时,所有节点都标记为已访问。
    • 查询操作:要查询某个节点的子树中已标记的节点数量,利用时间戳范围 [st[q], en[q]] 进行树状数组前缀和的差值查询。
    • 更新操作:根据节点的当前状态,更新树状数组以反映节点的访问状态变化。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
typedef long long ll; // 定义长整型别名
using namespace std;

ll n, m, tot, hd[100001], v[100001], c[100001], st[100001], en[100001], q;

struct node
{
    ll to, next; // 节点结构体,表示边的目标节点和下一条边的索引
} a[200001];

void add(ll x, ll y)
{
    a[++tot].to = y; // 添加一条从x到y的边
    a[tot].next = hd[x]; // 将该边的索引添加到邻接表中
    hd[x] = tot; // 更新邻接表头指针
}

ll lowbit(ll x)
{
    return x & (-x); // 返回x的最低位1表示的值
}

void change(ll x, ll y)
{
    for (; x <= n; x += lowbit(x))
    {
        c[x] += y; // 树状数组更新操作
    }
}

ll ask(ll x)
{
    ll ans = 0;
    for (; x; x -= lowbit(x))
    {
        ans += c[x]; // 树状数组查询操作
    }
    return ans;
}

void dfs(ll k)
{
    tot++;
    st[k] = tot; // 记录节点k的起始时间戳
    for (int i = hd[k]; i; i = a[i].next)
    {
        dfs(a[i].to); // 深度优先搜索,遍历所有子节点
    }
    en[k] = tot; // 记录节点k的结束时间戳
}

int main()
{
    cin >> n;
    for (ll i = 1; i <= n - 1; i++)
    {
        ll u, v;
        cin >> u >> v;
        add(u, v); // 建立树的邻接表
    }
    tot = 0;
    dfs(1); // 从节点1开始进行深度优先搜索
    for (int i = 1; i <= n; i++)
    {
        v[i] = 1; // 初始化所有节点为已访问状态
        change(st[i], 1); // 更新树状数组
    }
    cin >> m;
    for (ll i = 1; i <= m; i++)
    {
        char s;
        cin >> s;
        if (s == 'Q')
        {
            cin >> q;
            cout << ask(en[q]) - ask(st[q] - 1) << endl; // 查询节点q的子树中已访问节点的数量
        }
        else
        {
            cin >> q;
            if (v[q] == 0)
            {
                change(st[q], 1); // 如果节点q之前未访问过,则将其标记为已访问
                v[q] = 1;
            }
            else
            {
                change(st[q], -1); // 如果节点q之前已访问过,则将其标记为未访问
                v[q] = 0;
            }
        }
    }
    return 0;
}

3.时间复杂度

  • DFS遍历:O(n),遍历所有节点一次。
  • 树状数组操作:每次更新和查询操作时间复杂度为 O(log n),因此总操作复杂度为 O(m log n),其中 m 是操作的数量。
  • 21
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值