树上问题进阶

点分治

应用

  1. 树上所有路径统计
  2. 树上每个节点做根,信息统计。

注意:需要在求出重心后重新统计子树大小,别写假了。

P3085 [USACO13OPEN] Yin and Yang G

将两种颜色边权分别设置为 \(-1\)\(1\),转化为三段路径和为 \(0\)。如果统计中间的那个点和另一个端点的话需要用到乘法原理,有些麻烦而且无法排除同一端点不同中点的情况。可以对于端点分类:祖先有可以做中点的点和祖先没有可以做中点的点,然后分来统计即可。

P4886 快递员

思路新奇的题目,如果枚举所有点做根,然后统计是 \(O(n^2)\) 的。可是我们发现其实可以缩减计算量,假设当前以 \(c\) 为根,存在一个组 \((u_i,v_i)\) 使得两者是最长路径且经过 \(c\),或者存在至少两组 \((u_i,v_i)\) 使得 \(u_i\)\(v_i\) 在同一子树内,且两组点对不同子树。这样子就无法缩减答案了,否则进入那颗包含最大 \((u_i,v_i)\) 的子树。借鉴点分治思想,每次选取重心,复杂度是 \(O(n\log n)\)

P2664 树上游戏

法一:点分治
每次算出其他子树内包含颜色 \(i\) 的节点数 \(cnt_i\),然后计算其他子树内总节点数 \(tot\)。每到一个点先累加 \(\sum cnt_i\),然后对于 \(u \to rt\) 路径上的每种颜色累加 \(tot-cnt_c\)。同时传递一下标记。
法二:这里其实有线性做法。
从每个颜色角度考虑,包含某个颜色有点难处理,可以改为不含某颜色,只需要把那个颜色的点都去掉,形成多个连通块,给连通块内每一个点加上快的大小,代表从该点出发不包含某颜色的数量。答案也就是总数减去不包含的数量。每个颜色单独处理显然重复处理信息了。因为这不是图是树,而树上每个点就是割点,连通块的产生应该是很容易的,只需要某个点的连或断所以我们应该是可以把所有颜色一起处理的。
假设 dfs 到了点 \(u\) 颜色为 \(c\),那么便会产生一个 \(c\) 的连通块,这连通块到哪里终止呢,就是到另一个 \(c\) 的子树,于是我们只需要给除去 \(c\) 子树的部分加上 \(sz-\sum sz'\) 就行了,其中 \(sz\) 代表 \(u\) 子树的大小, \(sz'\) 代表有边为 \(c\) 的子树的大小。这个过程可以通过树上差分实现。别忘记对于 \(1\) 点单独处理一下。
小技巧:如何找到子树中所有颜色为 \(c\) 的点:利用 dfs 序加 vector 即可。

P4183 [USACO18JAN] Cow at Large P

设点的深度和到叶子节点的距离分别是 \(dep_i\)\(g_i\),一个点可以封锁的条件就是 \(dep_i \ge g_i\),可是我们要求封锁点尽量少,也就希望这个点尽可能往上,等价于儿子满足条件但父亲不满足这个条件。如果单纯是条件一的话可以用换根 dp 来求,可是每个点的父亲无法再换根,这就很难做。

观察一下我们会发现这里其实是一个子树产生 "1" 的贡献,这里有一个公式就是子树内 \(\sum(2-deg_i)=1\),于是我们只需要对于每个满足要求的点 \(u\) 产生 \(2-deg_u\) 的贡献即可。点分治统计点对 \((u,v)\) 的贡献。于是对于任意根节点 \(r\),求 \(\sum\limits_{d_{r,u\ge g_r(u)}}(2-deg_u)\)

注意一下,对于那个度数求和公式的使用,首先是无向树,其次注意是“子树”,所以子树的根向上还有一个度也要算。

CF833D Red-Black Cobweb

对于边占比的信息不太好直接合并出来,于是我们考虑维护 \(a-kb\) 形式的信息使得合并之后可以快速查看是否满足比例约束。

具体来说我们设黑边个数为 \(a\),白边个数为 \(b\),那么我们维护四个信息 \((A,B,C,D)=(a-2b,2b-a,b-2a,2a-b)\)

于是 \((u,v)\) 点对可以产生贡献必须满足,\(A_u\le B_v\)\(C_u\le D_v\)。这是一个二维数点的形式。

直接排序 \(+\) 树状数组统计即可。如果去除同一子树内贡献呢,我们对于当前分治重心下同一子树的信息单独跑一边上述计算减去贡献即可。但是这么做感觉有点问题,就同一点对会产生重复贡献,由于是乘法需要开根号,这就需要取模意义下的开根号也就是二次剩余有点麻烦。

为了防止点对产生重复贡献,我们可以赋一个顺序,也就是子树顺序,然后直接 CDQ 即可。

时间复杂度 \(O(n\log^3 n)\)

边分治

点分治产生多颗子树,但是有的时候信息难以合并,这个时候就要用到边分治这样只会产生两部分,方便操作。防止超时应该用多叉树转二叉树

BZOJ 2870. 最长道路tree

直接对于边 \(i\) 进行边分治即可,然后双指针扫描。

\[ans=\max((f_x+f_y+1+val_i) \times \min(g_x,g_y)) \]

动态点分治

就是多次询问的要用到点分治结构的东西。

我们对于点分治的每层重心之间连边得到的树就是点分树。

这样子树高是 \(O(\log n)\) 级别的,我们可以暴力跳点分树上的父亲来求解答案,注意细节需要去掉父子之间重复的信息。

P6329 【模板】点分树 | 震波

模板题。直接对于每个点 \(u\) 维护自点分树子树内距离自己为 \(k\) 的点权和,记为 \(C_{0,u,k}\)。查询就是不断跳父亲累加答案。思考一下为什么是这样子,因为这本质是一个路径统计问题,我们需要统计 \(u-x\) 距离 \(\le k\)\(\sum a_x\),路径统计可以类似点分治的方法解决,但是多次询问复杂度过高。在我们保存了各层分治中心之后,可以发现所有 \(u-x\) 路径必定可以拆分为 \(u-anc-x\),其中 \(anc\)\(u\) 在点分树上的祖先,根据点分治统计的完全性,可以知道这个方法是正确的,可以这么拆。

下面还要解决两个问题,一个是动态修改,很好办,直接把 \(C\) 替换为树状数组即可。还有一个就是 \(u,fa_u\) 之间的统计重复。我们可以设 \(C_{1,u,k}\) 表示在 \(u\) 子树内和 \(fa_u\) 距离为 \(k\)\(\sum a_x\)。每次更新点权的时候就直接暴力跳父亲,更新一路上的 \(C_0\)\(C_1\)。每次查询的时候就直接一边加 \(C_0\) 的贡献,一边减去 \(C_1\) 的贡献。

我们使用 vector 来储存 \(C\),对于 \(C_{0,u}\) 直接开 \(C\) 的最大深度大小,对于 \(C_{1,u}\) 要开 \(f_u\) 的最大深度大小。

P3676 小清新数据结构题

树链剖分

我们可以先动态维护根为 \(1\) 的时候的答案。有一个很简单的技巧就是把修改变为增加,对于一个点的修改影响的是所有祖先的子树和,于是我们只需要支持快速链加,全局求和即可。\(\sum (s_i+x)^2=s_i^2+2\times s_i\times x+x^2=\sum s_i^2+2x\sum s_i+len\times x^2\)。树链剖分维护即可。

然后考虑换根到 \(u\),不妨设路径为 \(1=u_0-u_1-...-u_k=u\)。跟为 \(1\)\(u\) 的时候子树和分为 \(a_i\)\(b_i\)
答案为 \(ans_u=ans_1-\sum\limits_{i=0}^ka_i^2+\sum\limits_{i=0}^kb_i^2\)

又因为 \(a_{i+1}+b_i=a_0=b_k\),可以得到

\[\sum\limits_{i=0}^kb_i^2=\sum\limits_{i=1}^k(a_0-a_i)^2+a_0^2 \]

拆开维护一波即可。

点分树

考虑将根从 \(1\) 一步一步移动到 \(u\),发现每次移动后都只有两个值会发生变化,也就是 \(s_{rt}\to sum-s_i\) 还有 \(s_i \to s_{rt}\)

于是我们可以发现 \(\sum(sum-s_i)\times s_i\) 为定值。

又因为 \(\sum s_i^2\) 正是我们需要求解的值,所以我们只需要维护 \(sum \times \sum s_i\) 即可。
\(sum\) 很容易维护,现在的问题就在 \(\sum s_i=\sum a_i\times (dis_{i,x}+1)\) 上面,这是动态点分治的模板。

在修改点权的情况下,\(\sum(sum-s_i)s_i\) 是会变化的,我们需要快速求值。考虑一下这个式子的意义,其实就是沿着 \(fa_i \to i\) 这条边把整个树划分成两个部分,然后两个部分的点对乘积和。再对于所有划分求和。

\[\begin{aligned} \sum(sum-s_i)s_i &= \sum\limits_{i=1}^n\sum\limits_{j=i+1}^na_i\times a_j\times dis(i,j) \\&=\sum\limits_{i=1}^n a_i \sum\limits_{j=i+1}^n a_j\times dis(i,j) \end{aligned} \]

单点修改,变化量就是 \((y-a_x)\times\sum\limits_{i=1}^na_j\times dis(x,j)\),又因为以 \(x\) 为根的树中,\(\sum s_i=\sum\limits_{i=1}^na_i\times (dis(i,x)+1)\)。所以直接维护即可。

P2056 [ZJOI2007] 捉迷藏

点分序

Link-Cut Tree

原树上每一个节点仅向其中一个儿子连实边。

每一个 Splay 维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历 Splay 得到的每个点的深度序列严格递增。所以 \(Splay(p)\) 之后只是保证 \(p\) 为平衡树的根节点,但并不是原树的根节点。需要处于最上方的平衡树且没有左子树保证深度最低才是根节点。

\(ch_{u,0/1}\) 表示 \(u\) 节点在平衡树上的左右儿子,他们左右儿子的 \(fa\)\(u\)。平衡树之间为虚边,满足认父不认子。也就是某平衡树的根节点 \(rt\)\(fa_{rt}\) 连向的点正是原树中那一条实链的父节点,但是由于不在一个平衡树内,所以父节点的 \(ch\) 中没有 \(rt\)

各个函数的总结

Splay 先将 \(u\) 到所在平衡树的根路径自上而下 pushdown,然后只要 \(u\) 不是根就重复执行旋转。也就是取 \(y,z\) 分别为 \(u\) 的父亲和爷爷。当然如果不存在爷爷就可以直接一步 rotate 到位了。如果 \(u,y,z\) 成一字型,我们连转两次 \(u\),否则先转一次 \(y\),再转 \(u\)。记得 while 之后再 pushup 一次,虽然我们 rotate 里面就有 pushup,但是防止 \(u\) 直接是根了,就进不去 rotate 了,同时 pushdown 带了的信息变动需要更新。

rotate 假设目前要旋转 \(x\),它的父亲和爷爷分别是 \(y\)\(z\)。根据认父不认子,我们通过 \(Nroot\) 来判断只要 \(y\) 不是平衡树的根节点,就把 \(z\) 的儿子修改为 \(x\)。然后对应更改一下 \(y,x\) 处的两个父子关系。最后不要忘记更新 \(fa\) 数组,同时记得判断节点非空再赋 \(fa\) 的值。最后先 \(pushup(y)\),再 \(pushup(x)\)

Nroot 用来判断是不是 Spaly 的根,根据虚边认父不认子的规则,我们只需要看它父亲的左右儿子中是否有它即可。

access 最重要的操作,将原树中 \(rt-x\) 路径上的边全部变成实边。分析一下,\(rt-x\) 的路径上一定是若干实链通过若干条虚边接在一起。实链内通过 Splay 来转,虚边间通过跳 \(fa\)。假设我们当前在 \(u\) 点,先 \(Splay(u)\) 使得 \(u\) 到达平衡树的顶点,发现此时 \(u\) 的右子树全部都是深度大于 \(u\) 点的部分,由于我们是要把 \(rt-u\) 变成实边,所以只和深度小于 \(u\) 的部分有关系,于是我们舍弃 \(u\) 的右子树,然后 \(u\) 的右子树正好连上下面需要接上的实链平衡树 \(v\)。为了将 \(u\) 继续往上接,我们 \(u\gets fa_u\)\(v\gets u\),继续执行上述过程即可。

makeroot 换根操作,就是先 \(access+splay\),把 \(x\) 转到 \(rt\) 所在平衡树的顶点,然后根据上述 \(access\) 舍弃右儿子的操作,我们可以发现 \(access(u)\) 之后,\(u\) 必定是其所在平衡树的最深点。也就是说第一步操作之后,\(x\) 只有左子树没有右子树,我们交换两个子树就完成了深度互换。

findroot (只合并不分离的情况下可以直接用并查集维护)。先 \(access(u)\) \(splay(u)\),这样子根节点就和 \(u\) 在一个平衡树内了,根据深度最小,我们不断跳左儿子即可。同时一定要每一步都 \(pushdown\)。为了保证树的形态,我们最好最后再把根通过 \(Splay\) 转回去。

split直接把 \(u\) 换成根节点,然后打通 \(v-u\) 的道路,最后把 \(v\) 转上去当平衡树的根,此时 \(v\) 维护的权值就是 \(u-v\) 链上的权值,上文提及的 \(access\) 操作保证无更深点可以保证没有其他点干扰。

link 对于保证不联通情况,直接 \(makeroot(u)\),再 \(fa_u\gets v\),注意第一步的原因是其中暗含 \(splay\) 操作,只有平衡树的根节点才能将 \(fa\) 赋值为实链顶的父亲,而且为了保证不损失原先 \(fa\) 信息,我们必须将其变成根节点。对于没有保证的,我们需要先 \(makeroot(u)\),再看一下 \(findroot(v)\),如果不连通再赋值 \(fa\)

cut 如果保证有边的话,我们直接 \(split(x,y)\)\(x\) 必然是 \(y\) 的左儿子,直接双向(\(ch,fa\))断开即可。如果不保证联通需要 \(makeroot(x)\),然后判断一下 \(findroot(y),lc(y),fa(y)\),最后再断开 \(ch,fa\)

lct维护子树信息

对于每个点记录所有轻儿子信息和,把 \(u\) 转成根后,就是平衡树上右子树了。
在 access 中 \(ch(x,1)=y\) 的时候 \(O(1)\) 修改。

P2387 [NOI2014] 魔法森林

暴力:枚举 \(a\),把所有满足 \(a\) 限制的边按照 \(b\) 升序排序依次加入并查集判断连通性。于是骗分做法:三分 \(a\) ,并查集求 \(b\) 最小值。或者两层二分 \(a~b\),然后并查集。但是正确性无法保证。

此时可以像 P5443 [APIO2019] 桥梁,那样操作分块+可回退并查集。其实感觉这题更本质一点,那题是针对询问和修改各有一套做法然后二者平衡一下,似乎询问操作二者还有点区别。但是这题其实让我弄懂了询问和操作本质没有区别,二者是完全一样的!!映射到桥梁那题中这题里的 \(A\) 就像询问,\(B\) 就像操作,我们发现其实可以 对枚举 \(A\) 加入 \(B\),显然也可以枚举 \(B\) 加入 \(A\),这是完全对称的!!这也就证明了那题中的询问和操作的做法其实本质是一样的本题对边关于 \(a\) 排序,然后块内对 \(b\) 排序,把所有之前 \(\le a\) 的边拿一个指针维护依次加入,问题就完美解决了。

回到LCT,这是动态树经典应用:不断地加边,判环,取最优者。同时这题还有一个小技巧就是边化点。定义一条边 \(i\) 的权值为 \(b_i\),本题做法就是从小到大枚举 \(a\), 然后尝试加入该边。如果 \(u~v\) 本来就不连通自然是加入成功了,如果二者联通,那么就在 \(u-v\) 路径上找到权值最大的点,如果该权值大于目前要加入边的权值,就把该边删除,加入新边。每次加完边后判断一下 \(1-n\) 是否联通,然后更新答案。这样整个 LCT 森林维护的就是在当当前约束 \(a\) 之下任意两点之间的最小 \(b\) 路径。

P8265 [Ynoi Easy Round 2020] TEST_63

集训的例题,质量挺高的,加深了对于 LCT 的理解。本题不是把 LCT 当工具使用,直接调用各个函数,而是对于其各个函数的实现进行了内部更改。

发现本题这些操作有点像 LCT,于是我们考虑用 LCT 的虚实边来维护树上的轻重边。也就是对应映射一下。那么每个 Splay 上面维护的便是一条重链的信息。

考虑将 \(y\) 变成 \(rt\) 的操作,是先 \(\operatorname{access}\)\(\operatorname{splay}\),再翻转。一次 \(\operatorname{access}\) 之后 \(y-rt\) 的所有边都变成了实边,对应一下也就是所有都变成了重边,但是显然不太符合实际情况。我们需要找到那些轻边在 LCT 中把他们变成虚边。因为根据轻边定义 \(sz_u > 2 sz_{son}\),所以对于每条轻边必然存在一个 \(k\) 使得 \(sz_u \ge 2^k > sz_{son}\)。可以枚举 \(k\) 通过 Splay 上二分找到 \(sz_u \ge 2^k > sz_{son}\),然后直接修。

对于统计重链第 \(k\) 大,在每次上述修改虚实边后一条重链会断成两段,我们将两段加入权值线段树中即可。

同时补充一下,对于上述 \(sz\) 的维护是在 LCT 中维护子树信息。做法是对于每个点记录所有轻儿子信息之和 \(gs\) ,改在 \(\operatorname{access}\)\(ch(x,1)=y\) 的时候 \(O(1)\) 修改即可。子树大小就是 \(gs(u)+s(ch(u,1))+1\)。同时本题规定了在子树大小相同的时候,选择下接重链上编号更大的那个作为重儿子,于是我们还要记录最大编号。

然后有一个卡常小技巧就是用两个优先队列来代替 set,一个优先队列负责添加元素,一个优先队列负责删元素,这样子常数小了好多。

下面是对于 LCT 一些传统的函数内部细节的说明。

\(\operatorname{push}\) 中维护的信息 \(sz\) 信息不只是平衡树上信息了,还要加上其他轻儿子信息 \(gs\)。维护 \(val\) 代表实链异或和,也就是重链异或和。同时记录一下该重链中最大编号。

void pushup(int u){
   s[u]=1+gs[u]+s[lc]+s[rc];
   Mx[u]=max({Mx[lc],Mx[rc],u});
   val[u]=val[lc]^val[rc]^u;
}

\(\operatorname{access}\) 中由于要修改 Splay 中右子树信息,这一项的修改涉及到了重链断裂成两个重链,同时新的重链生成。轻儿子的信息修改。于是我们记录下一路需要断开的边的位置,然后从上到下倒着修改一下。需要用到函数 modify(u,v)表示断开 \(u\) 原先的右子树,连上 \(v\)。首先,把原先保存的两条重链权值删掉。因为重链会发生变化。然后维护轻儿子信息,也就是轻儿子的子树信息和,还有 \(sz\) 的排名保存下来。然后在权值线段树中加入新产生的两条重链,注意第二条重链要在 \(\operatorname{pushup}\) 更新过新的重链信息后再加入权值线段树。当然对于 \(\operatorname{access}\) 造成的重链数量变多,我们在 \(\operatorname{link}\)\(\operatorname{cut}\)\(\operatorname{makeroot}\) 这些调用 \(\operatorname{access}\) 的函数里面再修改,而不是立刻修改。

void modify(int u,int v){
    seg.update(1,0,V,val[u],-1); if(v) 
    seg.update(1,0,V,val[v],-1);
    if(rc) S[u].ins(s[rc],Mx[rc]);
    if(v) S[u].del(s[v],Mx[v]); 
    gs[u]+=s[rc]; gs[u]-=s[v];
    if(rc) seg.update(1,0,V,val[rc],1);
    rc=v; pushup(u); seg.update(1,0,V,val[u],1); 
}

然后 \(\operatorname{cut}\) 的时候,要注意我们先要根据子树大小看看谁是谁的父亲,然后本来要判断一下 \(v\) 是否是 \(u\) 的轻儿子,但是我们可以通过 access(u)\(v\) 先变成 \(u\) 的轻儿子,这样子后面的操作就统一了。

\(\operatorname{link}\) 稍微修改一下,先记录一下轻儿子信息即可。

\(\operatorname{cut}\)\(\operatorname{link}\)\(\operatorname{makeroot}\) 函数后面都要跟着一个 change
函数,就是上面那个枚举 \(k\) 然后 Splay 上二分修改轻儿子的操作。

其余未提及函数均按照原先写法即可。

P3203 [HNOI2010] 弹飞绵羊

发现每个点只会跳向一个点,符合树上节点只有一个父亲,考虑用 LCT 来维护。我们从每个点向它跳向的点连边。

轻量级 lct。大概是一个树合并分离的 LCT 了。

发现不用换根等等一系列操作。

对于询问就是查询 \(rt-x\) 链节点个数。不需要用传统 \(split\) 函数了,因为只需要求到根的信息,直接 \(access+splay\) 解决。

\(link\) 函数,由于保证了 \(x\) 向外连的时候一定是树根,于是直接 \(splay\),然后赋值 \(fa\)

\(cut\) 函数,\(access+splay\) 之后去掉左儿子即可(双向断)。同时注意更改的 \(fa\) 信息是 \(ch_{x,0}\) 的而不是 \(x+k_x\) 的。

P4219 [BJOI2014] 大融合

LCT 维护子树信息模板题。

这里要维护的就是子树和了。

我们额外维护 \(gs_u\) 代表 \(u\) 的所有轻儿子的信息总和。pushup 内 \(s\) 的累加要带上 \(gs\)

\(access\) 往上跳的过程中也要动态调整 \(gs\)\(link\) 的时候由于加入轻儿子,所以也要修改 \(gs\),同时注意我们无法一层层往上更新,所以在 link 中更新 \(gs_v\),必须先 \(\operatorname{makeroot(v)}\)

每次查询 \((x,y)\) 就是先断开 \((x,y)\) 边,然后各自 \(\operatorname{makeroot}\) 求出两边各自的信息乘在一起,然后再连接上 \((x,y)\) 边。

此外,还有一种做法。根据最终形态建立出森林,然后求出 dfs 序。同时同一个并查集维护树上联通块,我们可以在每次加边的时候合并。同时大概要维护一个链加,求子树大小。可以用树状数组实现树上差分,子树大小就是子树求和了。

prufer 序列

基础知识

一个 \(n\) 个节点的唯一对应着一串长为 \(n-2\) 的序列,因此一个 \(n\) 阶完全图的生成树个数为 \(n^{n-2}\)

无根树转 prufer: 不断将无根树中编号最小的叶子节点删除,并将与之相连的节点放入序列中,直到只剩 \(2\) 个节点。具体方法是,维护一个指针 \(p\)\(1\) 开始扫描,如果 \(p\) 是叶子节点,就将 \(fa_p\) 放入序列中,并将 \(fa_p\) 的度数减一,判断 \(fa_p\) 是否为叶子节点,如果是就在 \(fa_p<p\) 的情况下继续重复上述操作。否则就 \(p \to p+1\),继续扫描。

prufer转无根树:每次找到点集中最小的没有在 prufer 序列中出现的数,与 prufer 序列中的第一个数连边,并将二者分别从点集和序列中删除。

性质: \(i\) 在 prufer 序列中的出现次数即为 \(deg_i-1\)

P2290 [HNOI2004] 树的计数

通过 prufer 序列上述的性质转化为可重集全排列。

\[\frac{(n-2)!}{\prod(d_i-1)!} \]

P2624 [HNOI2008] 明明的烦恼

记住如果使用 prufer 序列不用多想其他树上的事,只需要把数字填入序列即可。
本题先对有约束的点使用上一题中的公填入序列,再对剩下的空位置使用一次 prufer 序列。

P4430 小猴打架

形成的树有 \(n^{n-2}\) 种,同时 \(n-1\) 条边的连接还有顺序,所以还要乘上一个 \((n-1)!\)

CF156D Clues

技巧性杂题

  1. 树上问题可以转化为数点
  2. 树上差分

P5666 [CSP-S2019] 树的重心

反向考虑贡献,即考虑每个点作为重心的出现次数。
利用重心的数字意义数点。小技巧:这里用重心作为根,统计方便。
一条边去掉可能使得某个点 \(x\) 变成重心,当且仅当去掉的边不在该子树内,且该边去掉后形成的另一个树大小 \(S\) 满足 \(n-2s_x \le S \le n-2g_x\) 即可,其中 \(s_x\)\(x\) 子树大小,\(g_x\)\(x\) 子树最大的儿子节点数。对于每个点树状数组数点可以解决。这里有一些小细节。

  1. 如果去除子树内边的影响?重新开一个树状数组 \(c_2\) 动态计算,然后进入 \(u\) 前后的差就是子树需要减去的贡献。
  2. 去掉某边之后的 \(S\)\(n-s_x\) 还是 \(s_x\)?这时候需要如果边连着 \(u\) 那么进入 \(u\) 之后,边对应的 \(S\)\(n-s_x\),如果边与目前统计的点在不同子树内就是 \(s_x\)。我们发现大部分情况都是 \(s_x\),于是提前给树状数组 \(c_1\) 赋值 \(s_x\),然后进入 \(x\) 之后再修改为 \(n-s_x\)
  3. 如果处理根节点?只需要满足 \(s_{maxson} \le n-S\) 就行了,如果选取边在 \(maxson\) 之中,那么应该选次大与其比较。

P1600 [NOIP2016 提高组] 天天爱跑步

将观察者的询问挂在树上。
利用树上差分进行加减,同时为了统计出的个数为该子树内所产生的,可以在累加全局桶进入该子树前后的差。

长链剖分

链剖分规定:直链,链底端为叶子节点,每个点恰好包含于一条链中。
根据最深子树/最大子树分为长链剖分和重链剖分。

P10641 BZOJ3252 攻略

一条长链就是从叶子上来的一个条路径,我们选前 \(k\) 大长链即可。

树上启发式合并

很多时候都是对于每个树上每个节点及其子树求答案。
流程:计算轻儿子自己的答案(算完之后清楚),计算重儿子自己的答案(并保存中途数据),遍历轻儿子加入数据统计中,将自己加入数据统计,统计该点答案,如果该点是自己父亲的轻儿子就撤销统计。

CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

将每种颜色出现次数的奇偶性为二进制值,记 \(val_u\) 表示从 \(u\) 到根的异或和,树上差分思想,lca 处的贡献抵消,于是一条路径的贡献就是端点的异或和。

由于要求的是深度最大,于是我们维护 \(Mx_v\) 表示异或和为 \(v\) 的最大深度。按照 dsu on tree 的套路维护即可。

posted @ 2024-01-06 23:49  Mirasycle  阅读(10)  评论(0编辑  收藏  举报