题目大意
给定一棵树,可以进行以下三种操作:
- 选择一个结点v,使v对应的子树全部充满水。
- 选择一个结点v,除去v和v所有的祖先的水。
- 选择一个结点v,查询v是否有水。
题目分析
看完《挑战程序设计竞赛》的线段树章节后,搜了下codeforces的线段树题目来做,然后就搜到这题。这题做了很长时间哎,而且犯了不少很囧的错误。
首先看错了题,以为第二种操作是除去子树的水,所以很轻巧的敲了个BIT,跑起来不对才发现自己弄错了题意……
于是删掉代码重写。想到要更新所有祖先结点,岂不是要用树链剖分?参考了下cf的Tutorial,原来使用一个很巧妙的转化,就能变成普通的线段树问题。
我思考这题的时候,总是想着加水就是染成1,去水就是染成0,所以这两个操作分别对应子树的更新和路径的更新,这么做起来就很麻烦。Tutorial将思路倒转一下,便是去水只需要在v(最末端结点)上打个标记,加水是将子树上的所有标记去掉,查询某点是否有水是看这个点对应子树是否有标记,只要有一个标记,说明这点没水;没标记才表示有水。
而初始状态,我们将所有叶子结点打上标记,就可表示全没水的状态。
这样一来,确定dfs序之后,找出每个结点对应子树区间[L,R], 然后使用std::set即可实现这个算法,还很简单,都不需要线段树了。
cf上的前排代码求dfs时,将R[v] = cnt + 1
写成R[v] = ++cnt
我一开始很疑惑,这样写有什么区别,因为++后,就相当于对每个子树在末端新建了个虚拟的占位位置。
其实没区别。只是前排代码这么写,那么这一句S.erase(S.lower_bound(L[v]), S.lower_bound(R[v]))
的后半部分S.lower_bound(R[v])
既可以用lower_bound
, 又可以用upper_bound
, 我的代码就只能用lower_bound
source code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <set> #include <vector> #include <cstdio> using namespace std; const int maxn = 500010; vector<int> G[maxn]; int L[maxn], R[maxn], fa[maxn]; int cnt,n ,q; set<int> S; void dfs(int v, int par = -1) { L[v] = ++cnt; for (auto i:G[v]) { if (i != par) { fa[i] = v;dfs(i, v); } } R[v] = cnt + 1; if (R[v] == L[v] + 1) S.insert(L[v]); }
bool empty(int v) { auto it = S.lower_bound(L[v]); return it != S.end() && (*it) < R[v]; }
int main() { scanf("%d", &n); for (int i = 0; i < n - 1; i++) { int x,y; scanf("%d%d", &x, &y); G[x].push_back(y); G[y].push_back(x); } dfs(1); scanf("%d", &q); while (q--) { int c, v; scanf("%d%d", &c, &v); if (c == 3) puts((empty(v) ? "0" : "1")); else if (c == 1) { if (fa[v] && empty(fa[v])) S.insert(L[fa[v]]); S.erase(S.lower_bound(L[v]), S.lower_bound(R[v])); } else S.insert(L[v]); } }
|
Last updated: