题目描述
有一棵树,每个结点有一个灯(初始均是关着的)。每个灯能对该位置和相邻结点贡献1的亮度。现有两种操作:
(1)将一条链上的灯状态翻转,开变关、关变开;
(2)查询一个结点的亮度。
数据规模:\(1 \le n,q \le 10^5\)
简要题解
对于这种题,很容易想到任意指定一个根转化为有根树,每个结点维护值\(a_i\)表示它的所有儿子的贡献之和,这样再加上自己以及父亲的贡献就能回答一个询问了。
然而经过一波思考发现问题在于链修改时根本没法维护\(a_i\)。因此需要用链修改时的常规操作——树链剖分理论来解决本题了。
对于树链剖分,每一条链被剖分成了\(O(\log n)\)条重链,下面考虑修改每条重链\((s,t)\),不妨设\(depth[s]<depth[t]\)。可以发现修改后只会改变\((father[s],father[t])\)这条链上的\(a_i\),但是注意到链\((s,father[t])\)上每个结点都是它父亲的重儿子,如果我们\(a_i\)不维护重儿子,让\(a_i\)表示它的所有轻儿子的贡献之和,那么每条重链只需暴力修改一个\(a_{father[s]}\)。这样对于每个修改,更新\(a\)数组的时间复杂度为\(O(\log n)\);同时当然还需要更新链上灯的状态,即进行链异或1的操作,如果采用树链剖分加树状数组可达到\(O(\log ^2 n)\)的时间复杂度。对于每个询问\(x\),只需要查询\(a_x\)(对应轻儿子),以及\(father[x],x,son[x]\)(分别对应父亲、重儿子、自身)的状态即可(这里\(son[x]\)表示\(x\)的重儿子),查询时间复杂度\(O(\log n)\)。
总时间复杂度\(O(n+q \log ^2 n)\)。(PS:感谢队友ddd教我这么巧的方法!)
进一步的改进
这一做法虽然很巧妙,但是其唯一的瓶颈\(O(\log ^2 n)\)为树状数组修改,其它所有操作(无论\(a_i\)修改还是树状数组查询)均为\(O(\log n)\),显得很不协调(强迫症患者的我当然不舒服了)。针对本题链上异或单点查询问题,这里再给出一个更为高效的做法。
构造一个DFS序列,对每棵子树\(i\),序列定义为\(i\)开头,然后是\(i\)的每棵子树的DFS序列依次拼起来,最后\(i\)结尾,这样序列总长度为\(2n\)。设\(h_1[i]\)和\(h_2[i]\)分别为\(i\)第一次和第二次在序列中出现的位置。维护一个长为\(2n\)的数组\(b\),对于每次链\((x,y)\)异或1,执行如下算法:
(1)将\(b[h_2[x]]\)到\(b[2n]\)全部异或1;
(2)将\(b[h_2[y]]\)到\(b[2n]\)全部异或1;
(3)将\(b[h_2[lca(x,y)]]\)异或1。
定理:对于上述算法,每次单点查询\(x\)时,只需求\(b[h_1[x]] \oplus b[h_2[x]]\)即为答案。
为证明该定理,首先证明以下引理:
引理1:若结点\(t\)是结点\(x\)的祖先(可以是自身),当且仅当\(h_1[t] \le h_1[x]\)且\(h_2[t] \ge h_2[x]\)。
根据DFS序列的定义,证明显然。
引理2:对于结点\(x\),将\(b[h_2[x]]\)到\(b[2n]\)全部异或1后,只有\(x\)祖先\(t\)(包括自身)的\(b[h_1[t]] \oplus b[h_2[t]]\)值改变;
证明:首先证明充分性。若结点\(t\)是结点\(x\)的祖先,根据引理1,\(b[h_1[t]]\)不变,\(b[h_2[t]]\)异或了1,正确;
接下来证明必要性。若结点\(t\)不是结点\(x\)的祖先,根据引理1,分三种情况:
情况1:\(h_1[t] \le h_1[x]\)且\(h_2[t] < h_2[x]\),此时\(b[h_1[t]]\)和\(b[h_2[t]]\)均不变;
情况2:\(h_1[t] > h_1[x]\)且\(h_2[t] \ge h_2[x]\),此时必有\(h_1[t] > h_2[x]\)。若不然,根据\(h_2[x] \gt h_1[t]\)以及\(h_2[x] \le h_2[t]\)可知\(t\)是\(x\)的祖先,根据引理1,这与 \(h_1[t] > h_1[x]\)矛盾。因此\(b[h_1[t]]\)和\(b[h_2[t]]\)均异或了1;
情况3:\(h_1[t] > h_1[x]\)且\(h_2[t] < h_2[x]\),由于\(h_1[t] < h_2[t]\),故\(h_1[t] < h_2[x]\),此时\(b[h_1[t]]\)和\(b[h_2[t]]\)均不变。
以上三种情况\(b[h_1[t]] \oplus b[h_2[t]]\)均不变,必要性证毕。事实上,以上三种情况画图恰好对应了\(x\)左侧结点、\(x\)右侧结点和\(x\)的子树。
定理证明:根据引理2易得。
实现时,只需维护树状数组即可,每次链修改的时间复杂度为\(O(\log n)\)。这样整个题目的时间复杂度便被优化到了\(O(n+q \log n)\),已经达到了时间复杂度的极限,不可能再优了。
事实上,上述方法做出适当的修改可用于树上链加,单点求和的情形。广义来说,只要运算构成一个群,都可以用此方法优化时间复杂度。
AC代码
以下代码是改进之前的,更好写一些。
1 #include<cstdio> 2 #include<cstring> 3 #include<vector> 4 using namespace std; 5 vector<int> v[100001]; 6 int father[100001], depth[100001], top[100001], id[100001], son[100001]; 7 int cnt; 8 int dfs1(int i, int fa) 9 { 10 father[i] = fa; 11 depth[i] = depth[fa] + 1; 12 son[i] = 0; 13 int ret = 0, maxSize = 0; 14 for (unsigned int j = 0; j < v[i].size(); j++){ 15 int t = v[i][j]; 16 if (t == fa)continue; 17 int size = dfs1(t, i); 18 ret += size; 19 if (size > maxSize){ 20 maxSize = size; 21 son[i] = t; 22 } 23 } 24 return ret + 1; 25 } 26 void dfs2(int i, int tp) 27 { 28 top[i] = tp; 29 id[i] = ++cnt; 30 if (son[i])dfs2(son[i], tp); 31 for (unsigned int j = 0; j < v[i].size(); j++){ 32 int t = v[i][j]; 33 if (t != father[i] && t != son[i])dfs2(t, t); 34 } 35 } 36 void init() 37 { 38 cnt = 0; depth[0] = 0; 39 dfs1(1, 0); dfs2(1, 1); 40 } 41 42 bool tree[100001]; 43 int light[100001], treeLen; 44 int sum(int i) 45 { 46 int ret = 0; 47 for (; i > 0; i -= i&-i)ret ^= tree[i]; 48 return ret; 49 } 50 void flip(int i) 51 { 52 for (; i <= treeLen; i += i&-i) 53 tree[i] ^= 1; 54 } 55 void modify(int s, int t) 56 { 57 int top1 = top[s], top2 = top[t]; 58 while (top1 != top2){ 59 if (depth[top1] < depth[top2]){ 60 flip(id[top2]); flip(id[t] + 1); 61 t = father[top2]; 62 if (son[t] != top2)light[t] += sum(id[top2]) ? 1 : -1; 63 top2 = top[t]; 64 } 65 else{ 66 flip(id[top1]); flip(id[s] + 1); 67 s = father[top1]; 68 if (son[s] != top1)light[s] += sum(id[top1]) ? 1 : -1; 69 top1 = top[s]; 70 } 71 } 72 if (depth[s] > depth[t])swap(s, t); 73 flip(id[s]); flip(id[t] + 1); 74 if (son[father[s]] != s)light[father[s]] += sum(id[s]) ? 1 : -1; 75 } 76 int main() 77 { 78 int n, m, x, y, z; 79 scanf("%d%d", &n, &m); 80 for (int i = 1; i < n; i++){ 81 scanf("%d%d", &x, &y); 82 v[x].push_back(y); 83 v[y].push_back(x); 84 } 85 init(); treeLen = n; 86 while (m--){ 87 scanf("%d", &z); 88 if (z == 1){ 89 scanf("%d%d", &x, &y); 90 modify(x, y); 91 } 92 else{ 93 scanf("%d", &x); 94 int ans = father[x] ? sum(id[father[x]]) : 0; 95 if (son[x])ans += sum(id[son[x]]); 96 ans += sum(id[x]) + light[x]; 97 printf("%d\n", ans); 98 } 99 } 100 }