毛毛虫( 异或和,以及树的遍历)

毛毛虫

题目描述

树是一张nnn个点n−1n-1n1条边的无向连通图,每两个点之间都有唯一的一条简单路径。有根树是指以其中一个点为根节点的树,叶子节点是指除根节点外度数为 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,Q1≤n,Q≤500001 \leq n, Q \leq 500001n,Q50000)。
  • 第二行:n−1n-1n1个正整数f2,f3,…,fnf_2, f_3, \dots, f_nf2,f3,,fn1≤fi<i1 \leq f_i < i1fi<i),表示树上节点iiifif_ifi之间有一条边。
  • 第三行:QQQ个正整数a1,a2,…,aQa_1, a_2, \dots, a_Qa1,a2,,aQ1≤ai≤n1 \leq a_i \leq n1ain)。
  • 第四行:QQQ个正整数b1,b2,…,bQb_1, b_2, \dots, b_Qb1,b2,,bQ1≤bi≤n,ai≠bi1 \leq b_i \leq n, a_i \neq b_i1bin,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 = 0
  • 0 ^ 1 = 1
  • 1 ^ 0 = 1
  • 1 ^ 1 = 0
异或运算的性质
  • 交换律和结合律a ^ b = b ^ a(a ^ b) ^ c = a ^ (b ^ c)
  • 自身异或为 0a ^ 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),符合要求。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值