1.题目
描述
卡卡的房子外面有一棵苹果树。每年秋天,树上都会长出很多苹果。卡卡非常喜欢苹果,所以他一直在小心翼翼地培育这棵大苹果树。
这棵树有 N个叉子,这些叉子由树枝连接。Kaka 用 1 到 N对叉子进行编号,根始终用 1 编号。苹果会长在叉子上,两个苹果不会长在同一根叉子上。卡卡想知道一棵子树中有多少个苹果,用于研究苹果树的生产能力。
麻烦的是,有一段时间,一个新苹果可能会在空叉子上长出来,而卡卡可能会从树上摘下一个苹果作为他的甜点。你能帮忙卡卡吗?
输入
第一行包含一个整数 N (N ≤ 100,000) ,这是树中分叉的数量。
以下 N - 1 行各包含两个整数 u 和 v,这意味着 fork u 和 fork v 通过一个分支连接。
下一行包含整数 M (M ≤ 100,000)。
接下来的 M 行每行都包含一条消息,该消息是
“C x”,这意味着叉子 x 上的苹果的存在已更改。即如果叉子上有一个苹果,那么卡卡就摘它;否则,空叉子上长出了一个新的苹果。
或
“Q 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[]
。 - 时间戳:每个节点的子树可以通过时间戳范围来唯一标识。即子树中所有节点的时间戳都在该节点的起始和结束时间戳之间。
- DFS遍历:从树的根节点开始,遍历所有节点,并记录每个节点的起始时间戳
-
节点标记与查询:
- 节点标记:通过树状数组
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
是操作的数量。