线段树合并/分裂
线段树合并


线段树合并,通常用于动态开点的权值线段树中。在树上统计问题中较为常见,一般形式为:对 \(n\) 个结点中的每一个都建立一棵自己的权值线段树,预处理每个结点的线段树后,再 \(dfs\) 一遍整棵树,对每个结点的所有子树进行线段树合并。(合并后,每个结点的线段树维护的是其子树内所有点的信息;若预处理是在作差分,则合并相当于恢复差分,线段树维护的是每个结点自身的实际值)
核心函数 \(merge:\)
int merge(int nl, int nr, int u1, int u2){
if(!u1 || !u2) return u1 + u2;
if(nl == nr){ // 叶节点的数据直接合并到一起
sum[u1] += sum[u2];
}
else{ // 递归合并,最终值合并到u1上
int mid = nl + nr >> 1;
ls[u1] = merge(nl, mid, ls[u1], ls[u2]);
rs[u1] = merge(mid + 1, nr, rs[u1], rs[u2]);
pushup(u1);
}
return u1;
}
P4556 雨天的尾巴
树上路径加操作可以利用树上差分优化,将路径修改转化为仅在 \(4\) 个点上修改相应的权值线段树;最后还原差分做子树合并的过程中做线段树合并。
P3521
一道难题,需要在线段树 merge 操作真正执行前统计左右两个子树叶节点构成序列的逆序对数量,(这里先只考虑不交换,交换即将左右两子树编号交换)取左子树的 sum[rs] 和右子树的 sum[ls] 并相乘,得到在当前子值域中对逆序对数量的贡献,再继续递归累计更小值域内的贡献。
线段树合并优化 dp
P4577
先将所有点权离散化,使得值域为 \([1, m]\)。
状态定义
\(dp_{u,i}\):考虑以 \(u\) 为根的子树,其中选择的最小点权恰好为 \(i\),可选择的最多的点数量。
状态转移
决策子树 \(u\) 中选 \(u\) 或不选 \(u\)。
- 选 \(u\):
- 不选 \(u\):假设子树 \(u\) 已经合并了一些子树,现在正在合并子树 \(v\):
显然直接暴力转移的时间和空间复杂度都是 \(O(n^{2})\) 的。考虑用线段树合并来优化转移:
- 对于第一个转移,由于需要修改的只有 \(a_{u}\) 这一个单点,可以单独设计一个 update 函数来实现
- 对于第二个转移,相当于合并两棵线段树,可以在 merge 函数中实现。
实现细节
在 merge 中,有如下:
if(!u1 || !u2){
// u1 与 u2 其中某一个为空,则将非空结点打上懒标记,优化了区间加的操作
if(u1){
maxv[u1] += max2;
tag[u1] += max2;
}
if(u2){
maxv[u2] += max1;
tag[u2] += max1;
}
return u1 + u2;
}
这里使用了懒标记优化了区间加的操作,因为在线段树合并的过程中,若两个结点的其中一个为空,另一个非空,说明空结点不需要被转移,则按照第二个状态转移式,我们需要往非空结点上加上空结点的转移贡献,而它是一个定值,因此这步操作等价于区间加,可以打懒标记优化;否则,若两个结点均空,则无需操作;若两个结点均非空,则继续向下合并,直到到达了两个叶节点且均非空,则两个结点都含有转移信息,还需要将其中一个结点的信息合并到另一个结点上,也就是:
if(nl == nr){
maxv[u1] += max(maxv[u2], max2); // 不再是只加max2
}
\(max1, max2\) 分别表示两个线段树到当前结点表示区间的右侧的后缀,初始时当前结点表示整个区间,右侧并无区间,因此初值均设置为 0;在递归向下的过程中不断取右侧结点信息复合维护:
ls[u1] = merge(nl, mid, ls[u1], ls[u2], max(max1, maxv[rs[u1]]), max(max2, maxv[rs[u2]]));
rs[u1] = merge(mid + 1, nr, rs[u1], rs[u2], max1, max2);
每个点的 dp 值需要取选 \(u\) 和不选 \(u\) 两种情况的最大值,因此在 update 函数中的单点修改既不是单点加也不是单点赋值,而是需要与原值取最大值,表示两种情况的最优解:
int update(int u, int nl, int nr, int pos, int val){ // 单点修改 dp[u][pos] -> max(dp[u][pos], val)
if(!u) u = ++tot;
if(nl == nr){
maxv[u] = max(maxv[u], val); // !
}
else{
pushdown(u);
int mid = nl + nr >> 1;
if(pos <= mid) ls[u] = update(ls[u], nl, mid, pos, val);
else rs[u] = update(rs[u], mid + 1, nr, pos, val);
pushup(u);
}
return u;
}
最终答案即为:\(\max(dp_{1,1 \backsim m})\),更多细节见 code 部分。
P5298
备忘:
- 每个结点最多只有两个子节点
- 叶节点权值互不相同
先对所有权值做离散化,设值域为 \([1, m]\)。
由于题目给出的概率值既不可能完全等于 0,也不可能完全等于 1,因此很明显对于根节点 1,所有叶节点的取值是都可以取得到的。因此 \(m\) 就等于叶节点的个数,我们只需要计算根节点 1 对于每个叶节点取得其权值的概率。
状态定义
\(dp_{u,i}\):结点 \(u\) 权值为 \(i\) 的概率
答案即在 \(dp_{1,1 \backsim m}\) 中。
状态转移
考虑子树 \(u\):
- \(u\) 无子节点,即 \(u\) 是叶节点:
- \(u\) 只有一个子节点,设为 \(v\):
- \(u\) 有两个子节点,设为 \(a,b\):
- 当 \(u\) 以概率 \(p_{u}\) 取子节点最大值时:
- 当 \(u\) 以概率 \(1 - p_{u}\) 取子节点最大值时:
注意不需要考虑 \(a,b\) 权值相同的情况,因为 \(a,b\) 子树互不相交,包含的叶节点集合交集为空,而叶节点权值互不相同,因此 \(a,b\) 的取值一定不同。
会发现其实这和上一道题是类似的,只不过需要多维护一个前缀信息,与维护后缀信息的方法完全相同。
实现细节
在 \(a, b\) 的线段树合并函数 merge 中,需要维护两个复合信息 \(sum1, sum2\):
即上面的方括号式部分。
即上面的方括号式部分。
维护方法见代码。
在线段树合并递归向下的过程中,考虑 \(a, b\) 的两个子节点空或非空的情况:
- \(a,b\) 均为空:说明在当前区间内不存在信息供更新 \(u\) 的信息,因此直接返回。
- \(a\) 非空,\(b\) 空:只需要将 \(a\) 在当前区间内的信息填到 \(u\) 上,还需要乘上一个 \(sum2\)。这相当于区间乘,可以用乘法懒标记来优化。
- \(a\) 空,\(b\) 非空:只需要将 \(b\) 在当前区间内的信息填到 \(u\) 上,还需要乘上一个 \(sum1\)。这相当于区间乘,可以用乘法懒标记来优化。
- \(a,b\) 均非空:若还未到叶节点就继续递归向下走;否则在叶节点处直接合并 \(a,b\) 两个叶节点的信息。
更多实现细节见下方 code 部分。
P6773
线段树分裂
感觉用处不是很大,以后碰到题再去学。
模板题:



浙公网安备 33010602011771号