记录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_col 和 fin_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),轻松应对极限数据。