重链剖分学习笔记
前言
树链剖分(简称树剖)是一种将树剖分成若干链维护信息解决问题的思想。本文讲的是其中的重链剖分,着重介绍较为基础的内容,旨在帮助初学者更好地理解并掌握。
求 LCA
定义(斜杠表示本文中对其可能有多种表示方法):
-
为点 的深度(到根的边数)。 -
为以 为根的子树大小。 -
为点 的父亲。 -
为点 的一个儿子 满足 ,称为重儿子,一个点除重儿子外的儿子称为轻儿子。显然,一个点至多只有一个重儿子。 -
重边为点
与 之间的连边(如图中的 边)。轻边为树中除重边以外的边( 边):
-
重链为由连续的重边组成的极长链。特别地,若一个点没有重边与之相连,则也称这个点为重链。
-
为 所在重链中,深度最小的点,称之为链头。
upd:可以定义为子树大小最大的儿子。核心思想都是使得走一条连向父亲的轻边子树大小至少乘
可以通过 DFS 求出上述量:
void dfs1(int u,int fa){//求 sz[u],d[u],f[u]。
sz[u]=1;
for(int v:g[u]){
if(v^fa){
d[v]=d[u]+1;
dfs1(v,f[v]=u);
sz[u]+=sz[v];
}
}
}
void dfs2(int u,int fa){//求 h[u],t[u]。
for(int v:g[u]){
if(v^fa){
if((sz[v]<<1)>sz[u]){
t[h[u]=v]=t[u];
}else{
t[v]=v;
}
dfs2(v,u);
}
}
}
给出
个点的有根树,根为 。 次询问,每次询问点 的最近公共祖先。
。
考虑使用树链剖分。先看求 LCA 的代码:
int lca(int x,int y){
while(t[x]^t[y]){
if(d[t[x]]<d[t[y]]){
swap(x,y);
}
x=f[t[x]];
}
if(d[x]>d[y]){
swap(x,y);
}
return x;
}
该代码求解 LCA 的过程用两句话来讲就是:
-
若
在不同重链中,选取链头深度较大的点,让其跳过链头到达链头的父亲。 -
若
在同一重链中,深度小的点即为 LCA。
证明
定义:
-
为跳链操作,显然一个点能进行跳链操作必须满足其链头深度较大。特殊地,维护同一重链上的信息也称为跳链操作(方便后文表达)。 -
为 的 LCA,简称 LCA。
首先,最后跳到的节点一定是公共祖先,只需要证明它深度最大,即不是 LCA 的祖先。
考虑反证,假设某次跳链操作
说明 LCA 到
则
接下来分析复杂度。因为
所以,我们得到了一个时间复杂度为
树上问题转序列问题
给出
个点的有根树,根为 ,点有点权。有 次操作,支持:
,将 路径上的点的点权加 。
,求 路径上的点的权值和,模 。
,把以 为根的子树内的点权值加 。
,求以 为根的子树内的点的权值和,模 。
。
定义:
-
为点 的 DFS 序,简称编号/时间戳。 -
为 DFS 序中以 为根的子树的起始(时间戳最小)位置, 为结束(时间戳最大)位置。显然 。
首先,你是会用线段树求解区间加区间求和的。其次,若只有
void dfs3(int x, int fa) {
sg[st[x] = num[x] = ++sg[0]] = val[x] % p;
if (h[x]) {
dfs3(h[x], x);
}
for (int i : g[x]) {
if ((i ^ fa) && (i ^ h[x])) {
dfs3(i, x);
}
}
ed[x] = sg[0];
}
在这个代码中,若出现连续的重边,就会优先 DFS 重儿子,使当前重链的下一个点的编号为当前点编号加
为什么这样能够不重不漏地维护信息呢(笔者想硬胡一个理性理解,您要是觉得混淆还是跳过这部分,毕竟是个常识)?
-
对于最终跳到同重链上的两个点,会维护它们之间的信息(代码里可以实现)。
-
对于在重链上的点,跳到其重链之前会先维护它以下的信息,跳过它后会维护它以上的信息,因此不会重复。跳到当前重链时,这个点被包含在重链内,在线段树中维护的区间包括了它,因此不会漏。
之后就是区间问题了。由于跳链操作总数为
边转点技巧
给出
个点的树,边有边权,支持三种操作:
,将输入的第 条边的边权改为 。
,查询 两点路径上最大的边权。
,结束程序。
,操作数不超过 。
对于一棵树而言,儿子向父亲的连边一定是唯一的。因此,边转点的技巧就在于将点的权值设置为与父亲连边的边权。如图,
仍旧正常跳链查询,但是当
这一段链维护的信息实际上是
inline int answer(int x, int y) {
if (x == y) {//没有边。
return 0;
}
int ans = -1e9;
while (t[x] ^ t[y]) {
if (d[t[x]] < d[t[y]]) {
swap(x, y);
}
ans = max(ans, query(1, 1, n, dfn[t[x]], dfn[x]));//仍然正常维护链上信息,相当于维护 x 到 f[t[x]] 的边。
x = f[t[x]];
}
if (x == y) {//相等时,说明 x 到 LCA、y 到 LCA 的链都已经维护好(可以结合上图理解)。
return ans;
}
if (d[x] > d[y]) {//先令 y 为那个相同重链上不是 LCA 的点了。
swap(x, y);
}
return max(ans, query(1, 1, n, dfn[x] + 1, dfn[y]));//重儿子到 y。
}
回过头来解释一下为什么不用管根,因为若根作为 LCA,并且
修改就是单点修改那条边两端深度较大的节点,因为那条边的信息存在儿子上了。
记
习题:
Ⅰ - ABC294G Distance Queries on a Tree
给
个点的树, 次操作:
,将第 条边边权改为 。
,查询 之间路径的权值和。
。
唯一赛时会的 G。
运用边转点技巧,由于只有单点修改,并且是可差分信息,套上树状数组维护即可。
时间复杂度为
维护“存在性信息”小优化
给出
个点的树, 次询问 之间的路径和 之间的路径有没有公共点。
。
这是一种极其无脑的做法,就是码量比较大。我们直接将
维护这种存在性的信息,都可以采用类似的技巧优化。
时间复杂度
习题
Ⅰ - 洛谷 P3950 部落冲突
给出
个点的树, 次操作,有以下几种:
,查询 能否到达 。
,断掉 之间的边,保证 相邻。
,对于第 次 操作,恢复那条边。
。
先边转点,然后用
时间复杂度
结合“颜色限制”
给出一棵
个点的树,点 有权值 和颜色 ,有 次操作,分为以下类型:
,执行 。
,执行 。
,查询点 路径上, 的点 的权值和,保证 。
,查询点 路径上, 的点 权值的最大值,保证 。
, 。
如果没有颜色的限制就是树剖板子。加上颜色限制后,考虑对每一种颜色在树剖后的序列上维护线段树,然后查询就是跳链时在
delete
,会有玄学错误。
时间复杂度为
习题
Ⅰ - 洛谷 P5838 [USACO19DEC]Milk Visits G
双倍经验:SP11985 GOT - Gao on a tree。
有一棵
个节点的树,第 个节点有颜色 , 次询问,每次询问点 路径上是否存在颜色为 的节点。
, 。
每一种颜色分别在树链剖分后的序列上维护线动态开点线段树,将每个点在自己颜色的动态开点线段树上置
时间复杂度为
给出
个节点的树,每条边有颜色、边权。处理 个询问,第 次询问给出 ,表示先将所有颜色为 的边边权变成 ,再求出 到 的路径边权和。询问之间相互独立,即不会真的修改。
。
首先,路径上的边修改后才会影响答案。考虑树剖,先边转点。信息又和颜色有关,同样使用动态开点技巧在树剖后的序列上对每种颜色维护线段树,记录当前区间内有效边的个数
跳链时,先用前缀和查询原边权和,再在线段树上查询该区间的信息,记查询到的和为
时间复杂度为
Ⅲ - UVA12424 Answering Queries on a Tree
有
个节点的树。第 个节点颜色为 。有 次操作:
:把 改为 。
:询问 两点路径之间出现最多的颜色次数。
组数据, , , 。
注:相关变量名称和原题略有不同。
这题也是关于颜色限制的题,但是只有
先边转点,再对树剖后的序列用树状数组维护每种颜色的前缀个数。修改时,先将原颜色中该位置上
查询时,用
时间复杂度为
注:为了避免混淆,修改了部分变量的名称。
- 给出
个点的有根树, 种颜色,点 有颜色 ,颜色 有权值 。维护两种操作,共 次:
,将 。
,从树中等概率选取一个点 ,得到 的价值。求期望价值。其中 表示以 为根的子树中颜色为 的节点数。 是给定的常数。 。
先写出答案的表达式:
考虑维护每种颜色的
线段树的节点要维护平方和以及和,简单讲一下平方和的维护:
就是在原来平方和的基础上加上
对于
时空复杂度均为
给出
个点以 为根的树,点 有点权 。 次操作,两种类型:
,查询点 的一个最近祖先 ,满足 ,没有输出 。
,将 。
, 。
「保证
看到单点修改和路径查询,容易想到树链剖分。考虑到
由于根到某个点路径的 set
,里面有序存放点权有该质因数的点的 lower_bound
和 upper_bound
二分找到不超过当前点
对于修改操作,相当于在 set
中删除 set
中插入
乍一看分解质因数很暴力,但是我们可以在欧拉筛的时候处理每个数 set
,因此单次操作跳链查询/修改 set
次数是常数级别。
设值域为
给出
个点的树,边上有字符串 , 次询问 两点路径上有多少边的字符串以字符串 为前缀。
, 。
注意到
-
【方法一】
对每种字符串建立动态开点线段树,线段树上的区间维护对应的
序区间内该前缀的出现次数。跳链时在对应前缀的线段树上查询。时间复杂度为
,空间复杂度为 。 -
【方法二】
对每种前缀开一个
vector
,按从小到大的顺序存放具有该前缀节点的 序。跳链时,设当前跳到点 ,二分出 表示第一个大于等于 的值的位置, 表示最后一个不超过 的位置,则该重链的贡献为 。时间复杂度为
,空间复杂度为 。
相比之下,【方法一】空间更劣,常数更大,但是可以支持更多的修改操作。【方法二】空间较优,常数较小,但是在修改方面有一定的局限性。
给的是【方法二】的代码。
给出一棵
个点的树,每个点上有若干字符串,有 次操作,分为三种:
,查询 简单路径上的边数。
,查询 简单路径上字符串 出现了几次。
,查询 简单路径上字符串 出现了几次,并删除这些 。
,字符串总数 不超过 ,字符串长度不超过 。
字符串的种数很少,考虑使用 P5838 的套路,有两种方法:
-
【方法一】
对每种字符串维护一棵动态开点线段树,线段树的节点维护对应
序区间内该字符串的出现次数。-
对于
操作,跳链时在对应的线段树上查询当前重链的贡献。 -
对于
操作,则先做一遍 操作,然后再进行删除。删除的时候,在跳链时将当前重链对应的 区间推平成 。
时间复杂度为
,空间复杂度为 。 -
-
【方法二】
对每种字符串维护一棵平衡树(这里使用
__gnu_pbds::tree
),有序存放有该字符的点的 序,注意可能会重复,所以元素类型为pair
,second
的作用是区分同一个点的两个相同字符串。-
对于
操作,设当前在点 ,跳链时使用order_of_key
找到在 和 中的值个数,相减即为当前重链上该字符串的出现次数。 -
对于
操作,同样先做一遍 操作,然后再进行删除。删除的时候,使用lower_bound
找到第一个大于等于 的迭代器 和第一个大于 的迭代器 ,暴力删除 之间的所有迭代器。
看上去很暴力,但是一开始插入了
个元素,一个元素只能被删除一次,因此时间复杂度为 ,空间复杂度为 。 -
给的是【方法二】的代码。笔者因为看到 P7735 的一种新写法,即求出
维护树上子段信息
SP6779 GSS7 - Can you answer these queries VII
给出
个点的树,点 有点权 。给出 次操作:
,查询 路径上的最大子段和(就是路径上的点构成的连续序列的最大子段和,可以为 ,即空序列)。
,将 路径上的点权值赋值成 。
。
首先你是会区间最大子段和的,区间推平就是打懒标记,如果为正就取整个区间,否则取空区间。
考虑在树上怎么做,回顾区间最大子段和的过程,线段树非叶子节点的信息是由左右儿子的信息合并起来的。这里为了后文方便阐述,给一下合并区间信息的代码,稍微讲一下:
//ans 为合并的区间;l,r 为被合并的区间;sum 表示区间和;ret 表示区间最大子段和;lmax 表示区间最大前缀和;rmax 表示区间最大后缀和。
node merge(node l,node r){
node ans;
ans.sum=l.sum+r.sum;
ans.lmax=max(l.lmax,l.sum+r.lmax);//考虑信息是否横跨两个区间。
ans.rmax=max(r.rmax,r.sum+l.rmax);
ans.ret=max({l.ret,r.ret,l.rmax+r.lmax});
return ans;
}
那么对于一棵树,我们可以以 LCA 为界,将两边链的信息合并,如图中
在跳到同一重链之前,类似合并重链信息即可,跳到同一重链之后,分两种情况(图中涂上
还有一个小细节。合并完得到的两条链是这样的:
由于合并的是连续的序列,因此方向应该相同。所以对于
剩下几种情况同理即可。
时间复杂度为
习题
注:为了避免混淆,修改了原题中某些定义的名称。
给出
个点的树,树上可能会出现两种边:短边和长边。一开始所有边都是短边。有 次操作,两种类型:
,对于 路径上的任意一点,将所有与其相连的边变成短边,再将路径上的所有边变成长边。
,查询 路径上有多少条长边。
组数据, , 。
注:需要和“结合‘颜色限制’”区分,这里的颜色属于一类维护的信息,而非一种限制。
挺诈骗,和边转点没有一点关系。
首先要对问题进行一定的转化。有结论:钦定一开始树上所有点的颜色都是
考虑归纳证明,结论对于初始状态成立。由于没有端点在路径上的边在操作后是不会变的,因此只需要考虑以下几类情况:
-
与路径重合的边,操作后应该是长边。由于其两端点都在路径上,被染成了相同的颜色,所以是长边,结论成立。
-
一个端点在路径上的边,操作后应该是短边。若其之前是长边,说明之前两端点颜色都为
,则此时一个端点被染成了 ,两端点颜色不同,是短边,结论成立;若其之前是短边,则两个端点颜色分别为 ,此时无论替换哪个点的颜色为 ,两端点的颜色都是不同的,是短边,结论成立。
证毕。
看到路径操作首先想树剖,考虑如何对树剖后的序列维护区间内相邻的、颜色相同且不为
il node merge(co node&l,co node&r){//合并左右两个区间。
node ret;
ret.lc=l.lc;//左端肯定是左区间的左端。右端同理。
ret.rc=r.rc;
ret.tot=l.tot+r.tot;//不跨区间的情况,子区间的答案都要算。
if(l.rc&&r.lc&&l.rc==r.lc){//跨区间,如果符合条件就算上。
++ret.tot;
}
return ret;
}
查询就是跳链的时候合并重链信息。注意仍要想例题那样注意最终以 LCA 为界的两条链合并时的方向。
时间复杂度为
Ⅱ - 洛谷 P9555 「CROI · R1」浣熊的阴阳鱼
给出
个点的树,点有点权 。支持 次操作:
, 。
,你带着一个最大大小为 的可重集 从 走到 ,初始 ,遇到一个点 ,若 ,则删除 ,得分 ,否则将 插入 。求最终的得分。
。
看到树上修改和路径查询,首先想到树剖。我们发现
分析一下询问,相当于已经给定了初始状态
考虑如何合并区间信息,我们发现需要快速计算出已经到了一个区间末尾,下一步走到另一半区间开头时,
#define pii pair<int,int>
struct node{//变量名有部分不同。
int cnt_l[3][3],cnt_r[3][3],lc,rc;//f,g,lc,rc。
pii status_l[3][3],status_r[3][3];//LtoR,RtoL。
}seg[N<<2];
然后计算状态可以通过以下的函数实现(我写的比较暴力,直接枚举
#define ppi pair<pii,int>
#define mp make_pair
ppi get_status(pii p,int x){//第二维表示得分增量。
if(p==mp(0,0)&&!x){
return mp(p,0);
}
if(p==mp(0,0)&&x){
return mp(mp(0,2),1);
}
if(p==mp(0,1)&&!x){
return mp(mp(0,2),1);
}
if(p==mp(0,1)&&x){
return mp(mp(1,2),1);
}
if(p==mp(0,2)&&!x){
return mp(mp(0,0),0);
}
if(p==mp(0,2)&&x){
return mp(mp(2,2),1);
}
if(p==mp(1,1)&&!x){
return mp(mp(1,2),1);
}
if(p==mp(1,1)&&x){
return mp(mp(1,1),0);
}
if(p==mp(1,2)&&!x){
return mp(mp(2,2),1);
}
if(p==mp(1,2)&&x){
return mp(mp(1,1),0);
}
return mp(mp(x,2),0);//空集就直接放。
}
区间信息具体合并方法为,先从计算当前状态走到区间末尾的信息,然后计算跨过区间后的状态,以及计算跨区间这一步对得分的贡献。然后再从另一半区间,以跨区间后的状态开始走,计算得分。从当前起点走到另一个端点的状态,就是从另一半区间,以跨区间后的状态开始走,走到那个端点时的状态。代码如下:
#define fi first
#define se second
node merge(node l,node r){
node ret;
ret.lc=l.lc;
ret.rc=r.rc;
for(int i=0;i<=2;++i){
for(int j=i;j<=2;++j){
ppi l_start=get_status(r.status_r[i][j],l.rc),r_start=get_status(l.status_l[i][j],r.lc);
ret.cnt_l[i][j]=l.cnt_l[i][j]+r_start.se+r.cnt_l[r_start.fi.fi][r_start.fi.se];
ret.status_l[i][j]=r.status_l[r_start.fi.fi][r_start.fi.se];
ret.cnt_r[i][j]=r.cnt_r[i][j]+l_start.se+l.cnt_r[l_start.fi.fi][l_start.fi.se];
ret.status_r[i][j]=l.status_r[l_start.fi.fi][l_start.fi.se];
}
}
return ret;
}
对于长度为
seg[x].lc=seg[x].rc=b[l];//b[l] 是那个点的元素值。
for(int i=0;i<=2;++i){
for(int j=i;j<=2;++j){
seg[x].cnt_l[i][j]=seg[x].cnt_r[i][j]=0;//端点已经考虑过且没有遇到新元素,对得分无贡献。
seg[x].status_l[i][j]=seg[x].status_r[i][j]=mp(i,j);//起点终点相同,受到起点的影响即受到了终点的影响。
}
}
那么单点修改也很好维护:
seg[x].lc^=1;
seg[x].rc^=1;
查询就是跳链查询。注意合并信息的顺序。
时间复杂度为
给出一棵
个点的树。每个点有 两个区域,若不是障碍物,每次可以走到相邻点的相同区域,或当前点的另一个区域。
次操作,有两种类型:
,查询从 开始,向 的方向走,最多能不重复地访问多少区域。
,将 的区域状态改成 。
, 。
下文用
考虑重链剖分 + 线段树。
在线段树的一个节点内,维护:
-
表示从左端点 区域走到右端点 区域可以踩的最多格子数。若不能走到,则状态为 。 -
表示从右端点 区域走到左端点 区域可以踩的最多格子数。若不能走到,则状态为 。 -
表示从左端点 区域开始最多能走多远。 -
表示从右端点 区域开始最多能走多远。 -
表示左端点的地图形状。 -
表示右端点的地图形状。
信息可以这样合并,大概就是考虑是否走过区间中点,以及从哪个区域走过区间中点:
#define str string
#define co const
#define mst memset
struct node {
int f[2][2], g[2][2], a[2], b[2]; str l, r; bool e;
node(str l_ = "", str r_ = "", bool e_ = 1) {
l = l_; r = r_; e = e_;
mst(f, 0, sof f); mst(g, 0, sof g); mst(a, 0, sof a); mst(b, 0, sof b);
}
node operator+(co node o) const {
if (e) return o; if (o.e) return *this; node ret(l, o.r, 0);
for (int i = 0; i < 2; ++i) {
ret.a[i] = a[i]; ret.b[i] = o.b[i];
for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
bool op = (r[k] == '.' && o.l[k] == '.');
if (op && f[i][k] && o.f[k][j])
ret.f[i][j] = max(ret.f[i][j], f[i][k] + o.f[k][j]);
if (op && o.g[i][k] && g[k][j])
ret.g[i][j] = max(ret.g[i][j], o.g[i][k] + g[k][j]);
}
bool op = (r[j] == '.' && o.l[j] == '.');
if (op && f[i][j]) ret.a[i] = max(ret.a[i], f[i][j] + o.a[j]);
if (op && o.g[i][j]) ret.b[i] = max(ret.b[i], o.g[i][j] + b[j]);
}
}
return ret;
}
};
对于单点,有初始化:
void upd(node &o, co str s) { // 将叶子 o 的地图改成 s。
o = node(s, s, 0);
for (int i = 0; i < 2; ++i)
if (s[i] == '.') o.f[i][i] = o.g[i][i] = o.a[i] = o.b[i] = 1;
if (s[0] =='.' && s[1] == '.')
o.f[0][1] = o.f[1][0] = o.g[0][1] = o.g[1][0] =
o.a[0] = o.a[1] = o.b[0] = o.b[1] = 2;
}
只需要支持跳链查询、单点修改即可。注意跳链查询要翻转一条链。
时间复杂度为
结合斐波那契数列(矩阵快速幂)
定义
为斐波那契数列第 项,满足 (同时定义 时 )。 给出以
为根的 个点的树,每个点初始权值为 。 次操作,两种类型:
,将以 为根子树内的所有点 ,将其权值加上 。
,查询 路径上点的权值和,模 。
, 。
需要用到的前置知识:
Ⅰ -
证明
考虑归纳。
-
易证当
, 时成立。 -
假设在
时成立,则当 时:-
若
为奇数,则 , 均为偶数, 为奇数。有:成立。
-
若
为偶数,则 , 均为奇数, 为偶数。有:成立。
-
证毕。
Ⅱ -
证明
首先
考虑做差:
所以上面那个式子是成立的。
先钦定
-
易证
, 和 时均成立。 -
假设在
时成立,则当 时,在原值域上新增了三种情况:-
当
时:成立。
-
当
时(其实本质相同,但是为了完整还是写了):成立。
-
当
时:成立。
-
因此当
接下来考虑负数的情况:
假设
-
当
时:成立。
-
当
时:成立。
-
当
时:成立。
因此当
有了这两个结论以后,就可以开始做这道题了。
求斐波那契数列想到矩阵快速幂。路径查询问题先想树剖。将以
记
void down(int x){
if(tga[x]){//需要下传。
sum[ls]=(sum[ls]+va[ls]*tga[x]%M)%M;//答案加上 va[ls]*tga[x],注意这里要拿 a 值的和来乘,因为 tga[x] 是当前区间的系数,区间内每一项都要乘上这么多,根据乘法分配律就是给和乘上这么多。下面同理。
tga[ls]=(tga[ls]+tga[x])%M;
sum[rs]=(sum[rs]+va[rs]*tga[x]%M)%M;
tga[rs]=(tga[rs]+tga[x])%M;
tga[x]=0;
}
if(tgb[x]){
sum[ls]=(sum[ls]+vb[ls]*tgb[x]%M)%M;
tgb[ls]=(tgb[ls]+tgb[x])%M;
sum[rs]=(sum[rs]+vb[rs]*tgb[x]%M)%M;
tgb[rs]=(tgb[rs]+tgb[x])%M;
tgb[x]=0;
}
}
查询就是跳链查询。
时间复杂度为
离线技巧
洛谷 P4312 [COI2009] OTOCI / SP4155 OTOCI - OTOCI
给出一个
个点的图,一开始所有点都是孤点,点 有权值 。有三种操作,共 次:
,查询点 是否连通,若不连通,则在两点之间连边。
,将 。
,查询点 路径上的点权和,若不连通,输出 。
, 。
由于每次在不连通的点之间连边,因此最终图的形态肯定是一个森林,且每次询问的路径,若两点连通,他们的路径一定在森林上。因此考虑处理出
时间复杂度为
习题
Ⅰ - 洛谷 P2542 航线规划
给出一个
个点 条边的无向图,支持两种操作:
,询问 两点间的割边数。
,断掉 之间的边。 记操作总数为
, , , 。
删边不好维护,考虑离线倒序操作,变成加边操作和查询操作。回忆一下静态的两点间割边数怎么做,先建出边双树,然后答案就是两点边双连通分量之间的树上距离。故先离线建出边双树,树上边权置
来理解一些细节:
-
每次加边后,边双树的形态应当发生改边,但是两点树上路径可能包含失效的割边。其实是没有影响的,两点间存在失效的割边相当于已经存在了
边,再次推平成 并不影响,并且也可以把真正的割边置 。 -
查询会受到非割边的影响吗?其实是不会的,考虑那些
边,应当将这些边构成的极长链全部缩成一个点,然后统计剩下 边的贡献。但是在树上查询的时候, 边的贡献已经存在,非割边权为 对答案没有贡献。故这样做是对的。
时间复杂度为
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效