毛毛虫
题目描述
树是一张nnn个点n−1n-1n−1条边的无向连通图,每两个点之间都有唯一的一条简单路径。有根树是指以其中一个点为根节点的树,叶子节点是指除根节点外度数为 1 的节点。一个点的度数是指与其相连的点的个数。有根树上,一个点的深度是指其与根节点之间的简单路径的边数。
在某一棵以 1 为根的有根树上,有两个节点a,ba, ba,b上各存在一只毛毛虫。这两只毛毛虫只会往深度更大的点前进,当毛毛虫走到叶子节点时会停下。设第一只毛毛虫可能走到的节点为p1p1p1,第二只毛毛虫可能走到的节点为p2p2p2,你需要计算二元组(p1,p2)(p1, p2)(p1,p2)的个数(p1p1p1可以等于p2p2p2)。一共有QQQ次询问。
时间限制
- C/C++:1 秒
- 其他语言:2 秒
空间限制
- C/C++:256M
- 其他语言:512M
输入描述
- 第一行:两个正整数n,Qn, Qn,Q(1≤n,Q≤500001 \leq n, Q \leq 500001≤n,Q≤50000)。
- 第二行:n−1n-1n−1个正整数f2,f3,…,fnf_2, f_3, \dots, f_nf2,f3,…,fn(1≤fi<i1 \leq f_i < i1≤fi<i),表示树上节点iii与fif_ifi之间有一条边。
- 第三行:QQQ个正整数a1,a2,…,aQa_1, a_2, \dots, a_Qa1,a2,…,aQ(1≤ai≤n1 \leq a_i \leq n1≤ai≤n)。
- 第四行:QQQ个正整数b1,b2,…,bQb_1, b_2, \dots, b_Qb1,b2,…,bQ(1≤bi≤n,ai≠bi1 \leq b_i \leq n, a_i \neq b_i1≤bi≤n,ai=bi)。
- 第三行和第四行表示ai,bia_i, b_iai,bi是第iii个查询对应的两只毛毛虫所在的节点。
输出描述
为了避免输出量较大,你需要输出所有询问的答案的异或和。
示例
输入例子:
8 4
1 1 2 2 3 3 3
4 2 1 5
5 3 2 8
输出例子:
12
例子说明:
样例的树如下图(此处省略树图)。
四个询问对应的答案分别为 1, 6, 10, 1,将这些数字全部异或起来得到 12。
题解
#include <iostream> // 标准输入输出库
#include <bits/stdc++.h> // 包含常用标准库,如 vector
using namespace std;
using ll = long long; // 定义 long long 类型别名为 ll,处理大整数
const int M = 50010; // 定义节点数量上限
int c[M]; // 数组 c 存储每个节点子树中的叶子节点数量
bool vis[M] = {0}; // 数组 vis 标记节点是否被访问,初始为 false
vector<int> e[M]; // 邻接表 e[i] 存储节点 i 的所有邻接节点(父节点和子节点)
// DFS 函数:递归遍历树,计算每个节点子树中的叶子节点数量
void dfs(int x) {
vis[x] = 1; // 标记当前节点 x 为已访问
// 判断是否为叶子节点:如果当前节点邻接节点只有一个且该邻接节点已访问(即父节点)
if(e[x].size() == 1 && vis[e[x][0]]) {
c[x] = 1; // 叶子节点,连接的边只有父亲(被访问过)
return;
}
c[x] = 0; // 初始化当前节点子树叶子数量为 0
// 遍历当前节点的所有邻接节点
for(auto leaf : e[x]) {
if(vis[leaf]) continue; // 如果邻接节点已访问(父节点),跳过,避免回溯
dfs(leaf); // 递归遍历未访问的子节点
c[x] += c[leaf]; // 累加子节点的子树叶子数量到当前节点
}
return;
}
int main() {
// 读取输入:节点数量 n 和询问数量 Q
int n, Q;
cin >> n >> Q;
// 建树:从节点 2 开始,读取每个节点的父节点,构建双向边
for(int i = 2; i <= n; i++) {
int f;
cin >> f; // 读取节点 i 的父节点 f
e[i].push_back(f); // 在节点 i 的邻接表中添加父节点 f
e[f].push_back(i); // 在父节点 f 的邻接表中添加子节点 i
}
// 从根节点 1 开始 DFS,计算每个子树的叶子节点数量
dfs(1);
// 读取 Q 个询问的两组节点 a 和 b
int a[M], b[M];
for(int i = 0; i < Q; i++) {
cin >> a[i]; // 读取第一组节点(第一只毛毛虫起点)
}
for(int i = 0; i < Q; i++) {
cin >> b[i]; // 读取第二组节点(第二只毛毛虫起点)
}
// 计算异或和:对每组询问 (a[i], b[i]),计算 c[a[i]] * c[b[i]] 的异或累积结果
ll ans = 0; //和0 xor 还是自己。乘法是 1 ,加法是0
for(int i = 0; i < Q; i++) {
ll tmp = (ll)c[a[i]] * c[b[i]]; // 计算两节点子树叶子数量的乘积,即二元组 (p1, p2) 数量
ans = ans ^ tmp; // 将乘积结果与当前答案异或
}
// 输出最终异或和结果
cout << ans;
return 0;
}
DFS 遍历与子树叶子节点统计
DFS 的作用
DFS(深度优先搜索)用于遍历树结构,计算每个节点子树中的叶子节点数量。代码实现如下:
void dfs(int x) {
vis[x] = 1; // 标记当前节点为已访问
if(e[x].size() == 1 && vis[e[x][0]]) {
c[x] = 1; // 叶子节点,其子树叶子数量为 1(自身)
return;
}
c[x] = 0; // 初始化子树叶子数量为0
for(auto leaf : e[x]) {
if(vis[leaf]) continue; // 跳过已访问的节点(父节点)
dfs(leaf); // 递归遍历子节点
c[x] += c[leaf]; // 累加子树叶子数量
}
return;
}
遍历顺序是从根节点(节点 1)开始,递归向下遍历子节点。通过 vis 数组标记已访问节点,避免遍历回到父节点(树中无环,但邻接表是双向的)。对于非叶节点,累加所有子节点的 c[leaf] 值,得到子树总叶子数量。
关键逻辑
- 叶节点时,
c[x] = 1(自身)。 - 非叶节点时,
c[x]是所有子树叶子节点数量之和。 - DFS 确保每个节点只被访问一次,时间复杂度为
O(n)。 - 与之前代码的区别在于叶节点判断条件的细微变化,但核心逻辑一致,都是通过 DFS 自底向上统计叶子节点数量。
建树方式与邻接表的使用
建树过程
题目输入为每个节点的父节点(从节点 2 到 n),需要构建树结构:
for(int i = 2; i <= n; i++) {
int f;
cin >> f; // 读取节点 i 的父节点 f
e[i].push_back(f); // 在节点 i 的邻接表中添加父节点 f
e[f].push_back(i); // 在父节点 f 的邻接表中添加子节点 i
}
邻接表 e[i] 是一个 vector<int>,存储节点 i 的所有邻接节点(包括父节点和子节点)。树是无向图,父子关系在邻接表中双向存储,方便遍历。节点 1 是根节点,没有父节点,因此从节点 2 开始输入父节点。
特点
- 邻接表适合稀疏图(如树),空间效率高。
- 双向存储便于 DFS 时判断邻接关系,但需要通过
vis数组区分父子方向。 - 代码中建树逻辑清晰,确保了树结构的完整性,为后续 DFS 遍历奠定了基础。
异或和计算
在 C++ 中,异或运算使用符号 ^ 表示,是一种按位运算。它的作用是对两个整数的二进制位逐位进行比较:
- 如果对应位不同,结果为 1;
- 如果对应位相同,结果为 0。
异或运算规则
0 ^ 0 = 00 ^ 1 = 11 ^ 0 = 11 ^ 1 = 0
异或运算的性质
- 交换律和结合律:
a ^ b = b ^ a,(a ^ b) ^ c = a ^ (b ^ c)。 - 自身异或为 0:
a ^ a = 0。 - 与 0 异或不变:
a ^ 0 = a。 - 逆运算:如果
c = a ^ b,则c ^ b = a或c ^ a = b。
时间与空间复杂度分析
时间复杂度
- 建树耗时
O(n),处理n-1条边。 - DFS 遍历耗时
O(n),每个节点访问一次。 - 询问处理耗时
O(Q),处理Q组询问。 - 总时间复杂度为
O(n + Q),满足题目限制(n, Q <= 50000)。
空间复杂度
- 邻接表耗空间
O(n),存储2*(n-1)条边(双向)。 - 数组
c[M]和vis[M]各耗O(n)。 - 总空间复杂度为
O(n),符合要求。
965

被折叠的 条评论
为什么被折叠?



