P10722 [GESP202406 六级] 二叉树

记录121

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10;
vector<int> ch[MAXN];// 邻接表存储树结构,ch[i] 存放节点 i 的所有子节点编号
int op[MAXN],init_col[MAXN],fin_col[MAXN];
// op[i]记录节点i被直接操作的次数;init_col[i]存初始颜色;fin_col[i]存最终颜色
// DFS深度优先搜索函数:u是当前遍历到的节点,par_f是从根到当前节点父节点的累计翻转次数
void dfs(int u,int par_f){
	int total_f=par_f+op[u];// 当前节点的总翻转次数 = 祖先传递下来的翻转次数 + 自己被直接操作的次数
	if(total_f%2==1) fin_col[u]=1-init_col[u];// 如果总翻转次数是奇数,颜色取反(0变1,1变0)
	else fin_col[u]=init_col[u];// 如果总翻转次数是偶数,颜色保持不变
	for(int v:ch[u]) dfs(v,total_f);// 递归遍历所有的子节点v,将当前的总翻转次数传递给下一代
} 
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n,q;// n为节点数量,q为操作次数
	cin>>n;// 读入二叉树的节点数量 n
	for(int i=2;i<=n;i++){// 从第2个节点开始读入,因为根节点1没有父亲
		int par; // 临时变量,用于接收当前节点的父亲编号
		cin>>par;// 读入编号为 i 的节点的父亲节点编号
		ch[par].push_back(i);// 建立父子关系,把节点 i 加入其父亲 par 的子节点列表中
	}
	string col_str;// 定义字符串,用于接收长度为 n 的 01 串
	cin>>col_str;// 读入初始颜色字符串
	for(int i=1;i<=n;i++) init_col[i]=col_str[i-1]-'0';
	// 将字符串中的字符转换为数字存入数组(字符串下标从0开始,节点编号从1开始,减去字符'0'得到真实数值)
	cin>>q;// 读入操作次数 q
	for(int i=1;i<=q;i++){// 循环 q 次,处理每一次操作
		int node;// 临时变量,用于接收被操作的节点编号
		cin>>node;// 读入本次操作选择的节点编号
		op[node]++;// 将该节点被直接操作的次数加一(利用奇偶性,不需要立刻修改整棵子树)
	}
	dfs(1,0);// 从根节点(编号为1)开始进行 DFS,初始时祖先传递下来的翻转次数为 0
	for(int i=1;i<=n;i++) cout<<fin_col[i];// 依次输出每个节点的最终颜色
	return 0;//结束程序 
}

前言

我是一名专注信奥赛(CSP-J/S、NOIP)的教练。

  • 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
  • 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

题目传送门https://www.luogu.com.cn/problem/P10722


核心思路分析

这是一道经典的树形结构问题,如果采用暴力的方式(每次操作都遍历子树进行颜色反转),在数据规模达到 10^5 时必然会超时。本题的核心在于**"延迟更新"** 和**"状态传递"**的思想。

1. 翻转的奇偶性本质

  • 对一个节点的颜色进行两次反转,等于没有操作。因此,我们完全不需要关心一个节点被反转了多少次,只需要关心它被反转的次数是奇数还是偶数 。这在代码中可以通过简单的 ++ 运算配合 %2 取模来实现。

2. 祖先与后代的关系(自顶向下传递)

  • 题目规定:反转以节点 xx 为根的子树。这意味着,如果一个节点被反转了,那么它的所有子孙节点都会受到这次反转的影响。
  • 因此,对于树上的任意一个节点,它的最终状态由两部分决定:自身的初始颜色 + 从根节点到该节点路径上所有被直接操作的次数总和

3. DFS 深度优先搜索的应用

  • 我们可以先记录每个节点被直接操作 的次数(存入 op[] 数组)。
  • 然后从根节点开始进行一次 DFS。在 DFS 的过程中,维护一个变量 par_f(代表当前节点的父节点及以上所有祖先累计下来的翻转次数)。
  • 到达当前节点 uu 时,总翻转次数 total_f = par_f + op[u]。根据 total_f 的奇偶性,结合初始颜色,就能瞬间算出该节点的最终颜色。
  • 接着,将当前的 total_f 作为新的 par_f 传递给所有的子节点。这样一次 O(N)O(N) 的遍历就完美解决了所有 Q 次操作带来的影响!

代码逐行深度解析

第一部分:头文件、常量定义与全局变量
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+10;
vector<int> ch[MAXN]; // 邻接表存储树结构,ch[i] 存放节点 i 的所有子节点编号
int op[MAXN], init_col[MAXN], fin_col[MAXN];
// op[i]记录节点i被直接操作的次数;init_col[i]存初始颜色;fin_col[i]存最终颜色

解析 :这里使用了 vector 数组来构建动态的邻接表,非常适合存储树的父子关系。三个一维数组分工明确:op 负责记录局部操作,init_colfin_col 分别负责状态的起点和终点。

第二部分:DFS 核心递归函数
cpp 复制代码
// DFS深度优先搜索函数:u是当前遍历到的节点,par_f是从根到当前节点父节点的累计翻转次数
void dfs(int u, int par_f){
	int total_f = par_f + op[u]; // 当前节点的总翻转次数 = 祖先传递下来的翻转次数 + 自己被直接操作的次数
	
	if(total_f % 2 == 1) fin_col[u] = 1 - init_col[u]; // 如果总翻转次数是奇数,颜色取反(0变1,1变0)
	else fin_col[u] = init_col[u];                     // 如果总翻转次数是偶数,颜色保持不变
	
	for(int v : ch[u]) dfs(v, total_f); // 递归遍历所有的子节点v,将当前的总翻转次数传递给下一代
} 

解析 :这是整个算法的灵魂。通过参数 par_f 实现了状态的自顶向下无缝传递。利用 C++11 的范围 for 循环 for(int v : ch[u]) 使得代码更加简洁优雅。1 - init_col[u] 是一个非常巧妙的位运算替代写法,完美实现了 0 和 1 的反转。

第三部分:主函数入口与 IO 加速
cpp 复制代码
int main(){
	ios::sync_with_stdio(false); // 关闭 C++ 标准输入输出流与 C 标准库的同步
	cin.tie(0);                  // 解除 cin 和 cout 的绑定
	int n, q;                    // n为节点数量,q为操作次数
	cin >> n;                    // 读入二叉树的节点数量 n

解析 :面对高达 10^5级别的数据量,标准的 cin/cout 可能会因为底层缓冲区的刷新导致超时。这两行加速代码是应对大数据量输入输出的标配。

第四部分:建树与读取初始颜色
cpp 复制代码
	for(int i = 2; i <= n; i++){ // 从第2个节点开始读入,因为根节点1没有父亲
		int par;                 // 临时变量,用于接收当前节点的父亲编号
		cin >> par;              // 读入编号为 i 的节点的父亲节点编号
		ch[par].push_back(i);    // 建立父子关系,把节点 i 加入其父亲 par 的子节点列表中
	}
	string col_str;              // 定义字符串,用于接收长度为 n 的 01 串
	cin >> col_str;              // 读入初始颜色字符串
	for(int i = 1; i <= n; i++) init_col[i] = col_str[i-1] - '0';
	// 将字符串中的字符转换为数字存入数组(字符串下标从0开始,节点编号从1开始,减去字符'0'得到真实数值)

解析 :题目给出的是每个节点的父节点,我们通过 ch[par].push_back(i) 将其转化为正向的邻接表。同时,巧妙利用 ASCII 码的特性 col_str[i-1] - '0',将字符 '0'/'1' 快速转换为整型 0/1

第五部分:处理操作序列与启动 DFS
cpp 复制代码
	cin >> q;                    // 读入操作次数 q
	for(int i = 1; i <= q; i++){ // 循环 q 次,处理每一次操作
		int node;                // 临时变量,用于接收被操作的节点编号
		cin >> node;             // 读入本次操作选择的节点编号
		op[node]++;              // 将该节点被直接操作的次数加一(利用奇偶性,不需要立刻修改整棵子树)
	}
	dfs(1, 0);                   // 从根节点(编号为1)开始进行 DFS,初始时祖先传递下来的翻转次数为 0
	for(int i = 1; i <= n; i++) cout << fin_col[i]; // 依次输出每个节点的最终颜色
	return 0;                    // 结束程序 
}

解析 :在处理 Q 次操作时,代码仅仅做了 op[node]++,将时间复杂度从 O(Q×N)O(Q×N) 骤降至 O(Q) 。最后通过一次 O(N) 的 DFS 统合所有信息并输出结果,整体算法的时间复杂度为完美的 O(N+Q),轻松应对极限数据。

相关推荐
smj2302_796826521 小时前
解决leetcode第3948题字典序最大的MEX数组
python·算法·leetcode
不负岁月无痕2 小时前
STL-- C++ stack_queue _priority_queue类 模拟实现
开发语言·c++
selt7912 小时前
Redisson 源码深度分析
java·c++·redis·lua
周末也要写八哥2 小时前
浅谈:C++中cpp 14 ~ cpp 17
开发语言·c++·算法
不会C语言的男孩2 小时前
C++ Primer 第13章:拷贝控制
开发语言·c++
c238562 小时前
map和set
数据结构·c++
basketball6162 小时前
C++进阶:3. unique_ptr 现代C++内存管理的基石
java·jvm·c++
FFZero12 小时前
[mpv脚本系统] (三) C 函数如何注册成 Lua 模块
c++·音视频·lua
我不是懒洋洋2 小时前
从零实现一个Redis客户端:RESP协议与网络编程
开发语言·c++