CF877E Danil and a Part-time Job 题解
题目大意
给定一棵具有个节点的树,以号点为根节点,且每个点的权值都为或。现在要求进行次操作,get x
代表询问节点的子树中有多少个点的权值是,pow x
代表将节点的子树中的所有节点取反。要求对于每个get
指令给出答案。
思路
对一棵树进行维护显然十分不方便,我们考虑将树的每个节点转换为一个区间,每个叶子节点转化成一个值,对区间的值使用线段树进行操作。用线段树进行区间维护需要满足的前提是区间的可加性,而一棵树所有子节点的和的确满足这个特征。
那么如何将一棵树转化成一个线性序列以方便使用线段树维护呢?我们可以利用求解树的dfs序来建立树上的节点与序列下标之间的映射。映射完之后我们就可以把这题当作一道板子来写了。
(1) 求解树的dfs序
关于树的dfs序在这篇炒冷饭的题解里就不详细解释了~~(其实是因为其他大佬讲得比我详细得多)~~,用简单的一句话来说就是在从根节点对一棵树进行dfs时每个节点被遍历到的顺序。以下图所示的一棵树举例:
这棵树的dfs序是0->1->2->6->4->5->7->8->3,通过观察我们可以发现,一个节点以及其子树上的节点在树的dfs序里总是连续的,因为dfs的特点是必须遍历完上一个节点的整棵子树才能遍历下一个节点。利用这一个性质,在这题里我们可以对这棵树预先进行dfs,建立树的节点与序列下标的映射:
1 | pair<int,int> range[200005]; |
代码中range[p].first
代表节点在序列中出现的位置,range[p].second
代表对的子树继续进行dfs时被遍历到的最后一个叶子节点。如果将这段代码运行并输出range
数组中的每一个值,则毫无疑问range[1].first
的值为2(即dfs序中的第二项),range[1].second
的值为6(即,5是1的子树上最后一个被遍历到的子节点)。求解完dfs序之后,我们建立线段树,对每个节点所对应的序列左右下标之间的值进行操作就可以了。
(2) 建树
这题的建树比较简单,读到某个节点的值是1就调用一次build()
进行单点修改即可:
1 | void build(int t,int nl,int nr,int p){ |
(3) 修改与查询操作
pushdown
本题中的延迟标记只有两种类型:mark[p]
的值为1代表点的子树需要进行取反,反之则不需要。关于取反操作直接将区间的值改为即可,因为本来权值为1的节点都变成了0,剩下的节点权值都变成了1。
1 | void push_down(int l,int r,int p){ |
区间修改&区间查询
和模板区别不大,直接上代码:
1 | void change(int l,int r,int nl,int nr,int p){ |
总结
我认为本题的主要思维难点就在于如何将树转换成容易维护的序列,之后直接套线段树模板即可,是一道很适合初学线段树时练手的题,思维难度和代码量都适中,同时需要注意的细节也有一些。咱写这篇题解的目的也是为了更好地梳理自己的做题思路,以后再遇见同类型的题就能更加得心应手。
以上。
完整代码
1 |
|