P6242 【模板】线段树 3
题意
维护一个数组, 支持一下五种操作:
- 区间加减
- 区间抹平(使所有大于
v
的数变成v
) - 区间求和
- 区间最值
- 区间历史最值
区间加减
普通线段树的基础操作, 需要用 LazyTag
保证效率, 无需多提.
区间抹平
朴素方法是遍历所有和操作区间有交的节点, 一直递归到叶子, 对每一个元素进行判断和操作.
尝试优化, 记录节点最大值, 次大值, 最大值个数, 如果 v
在最大值和次大值之间, 就使其最大值发生改变, 其余元素不受影响, 再结合最大值个数修改总和即可.
需要注意是, 这样需要引入一个新的 LazyTag
, 记录最大值的变化, 因为这项操作可以使最大值的变化和非最大值的变化不再同步.
区间求和
和一般线段树一样, 递归调用有交集的节点, 与此同时下传标记保证调用到节点的 Sum
值是最新的, 和普通线段树的区别是要下传两个标记而不是一个.
区间最值
非常简单, 由于维护了节点最大值 Mx
, 所以操作和求和类似, 只是答案合并时是取最大值而不是求和.
区间历史最值
这是使本题直接提升一个难度等级的操作, 很多人 (就是我) 一看就给节点加了一个记录历史最值的变量 HisMx
, 每次更新 Mx
就尝试更新一下 HisMx
.
但是这样做是不对的, 在调试了十余小时候我终于明白了原因.
一个区间的 LazyTag
, 不一定每次下传时就是最大值. 即下传一个标记时它可能已经被修改好几次了, 但是光看这个 Tag
值没人知道它最大时是多少.
这时要查询它的子区间历史最值了, 把它的最大值修改标记 Tagmax
标记一下传, 算出一个子区间的 Mx
一通修改后的最终值, 但是不一定是最大值. 导致了历史最值的错误.
HisMxTag
如果定义一个变量 HisMxTagmax
, 记录这个节点从上一次下传到本次下传屡次修改的最大值, 每一次 Tagmax
被修改都尝试更新 HisMxTagmax
, 将它用于历史最值的计算, 就能在下传时得到从上一次下传到现在几次修改中 Tagmax
最大的历史值了.
当然, 由于一个节点的子节点中不一定含有其父的最大值, 自然不能和父亲用同一个 Tagmax
, 所以为了得到其历史最大的最大值变化标记, 其父需要一个记录历史最大的非最大值变化标记 HisMxTagpls
, 用来传给最大值比自己最大值小的儿子.
复杂度
由于证明复杂度需要的知识过于高端, 这里只说结论,
对于本体数据规模时间复杂度是正确的.
实现
以上就是本体的基本思路, 接下来分部分放出代码逐个进行解剖.
存储
为了节省空间, 将区间端点作为函数参数递归维护
int a[500005]; //初始值数组
unsigned int M, n;
long long Lst, A, B, C, D; //全局变量存操作参数和答案防止调用函数时浪费时间
struct Node {
Node *L, *R; //左右儿子指针
int Tagpls /*非最大值变化标记*/, Mx /*最大值*/, HisMx /*历史最值*/,
SecMx /*次大值*/, Cntmax /*最大值计数*/, Tagmax /*最大值变化标记*/,
HisMxTagpls /*非最大值变化标记的历史最值*/,
HisMxTagmax /*最大值变化标记的历史最值*/;
long long Sum; //总和, 不开long long见祖宗
} N[2000005], *Cntn(N); // 4倍内存池 + 指针
下改上 Udt
儿子的 LazyTag
永远不影响父亲, 所以只要修改 Sum
, Mx
, SecMx
, HisMx
即可.
对 Mx
, SecMx
分几种情况讨论, 代码已经注释得很清楚了.
void Udt(Node *x, const unsigned int &l, const unsigned int &r,
const unsigned int &m) { //儿子更新父亲
x->Sum = x->L->Sum + x->R->Sum; //总和为两儿子的和
x->HisMx =
max(x->L->HisMx, x->R->HisMx); //历史最大值为两儿子历史最大值的较大的一个
if (x->L->Mx == x->R->Mx) { //更新最值和最值数量, 两个儿子最大值相等
x->Cntmax = x->L->Cntmax + x->R->Cntmax; //最大值计数为两儿子之和
x->Mx = x->L->Mx; //最大值就是两儿子最大值
x->SecMx =
max(x->L->SecMx, x->R->SecMx); //次大值是两儿子次大值中的较大的一个
return;
}
if (x->L->Mx > x->R->Mx) { //最大值在左儿子上
x->Mx = x->L->Mx; //最大值是左儿最大值
x->Cntmax = x->L->Cntmax; //最大值计数是左儿最大值计数
x->SecMx =
max(x->R->Mx, x->L->SecMx); //次大值为右儿最大值和左儿次大值较大的那个
return;
}
x->Mx = x->R->Mx; //最大值在右儿上
x->Cntmax = x->R->Cntmax; //最大值计数是右儿最大值计数
x->SecMx =
max(x->L->Mx, x->R->SecMx); //次大值为左儿最大值和右儿次大值中较大的那个
return;
}
建树 Bld
利用刚刚写的 Udt
, 建树除了初始化的变量多了, 结构上和一般线段树没什么两样.
void Bld(Node *x, unsigned int l, const unsigned int &r) {
x->Tagpls = 0;
x->Tagmax = 0;
x->HisMxTagpls = 0;
x->HisMxTagmax = 0; //初始化标记
if (l == r) { //叶子
x->Cntmax = 1; //最大值有1个
x->SecMx = -0x3f3f3f3f3f3f3f3f; //无次大值
x->Sum = a[l];
x->Mx = a[l];
x->HisMx = a[l]; //总和, 历史最大值, 最大值都是当前单点的初始值
return;
}
unsigned int m((l + r) >> 1); //非叶节点两个儿子的分界点
Bld(x->L = ++Cntn, l, m); //左儿
Bld(x->R = ++Cntn, m + 1, r); //右儿
return Udt(x, l, r, m); //两儿更新x
}
打标记 PsCm
大部分的线段树, 由于 LazyTag
操作简单, 一般直接将打标记写在单点修改和标记下传中, 但是由于本题 LazyTag
十分复杂, 适合封装进一个小函数.
提示, 一个节点的标记对自己无效, 当前节点标记被更新都是为了传给儿子, 自己的各种值在父亲的标记下传时已经被更新了.
void PsCm(Node *x, const int &tgp /*非最大值标记*/,
const int &tgm /*最大值标记*/,
const int &hmtp /*从上一次下传以来历史最大的非最大值标记*/
,
const int &hmtm /*从上一次下传以来历史最大的最大值标记*/,
const unsigned int &l,
const unsigned int &r) { //传来的标记对当前节点的影响
x->HisMxTagpls =
max(x->HisMxTagpls, x->Tagpls + hmtp); //尝试更新历史最大的非最大值的标记
x->HisMxTagmax =
max(x->HisMxTagmax, x->Tagmax + hmtm); //尝试更新历史最大的最大值标记
x->HisMx =
max(x->HisMx, x->Mx /*上一次更新时的最大值*/ +
hmtm /*从上一次更新到现在出现过的最大的最大值标记*/);
x->Tagpls += tgp; //非最大值标记更新
x->Tagmax += tgm; //更新最大值标记
x->Mx += tgm; //更新最大值
x->Sum += (long long)tgm * x->Cntmax /*最大值对总和的贡献*/ +
(long long)tgp * (r - l + 1 - x->Cntmax) /*非最大值对总和的贡献*/;
x->SecMx += tgp; //更新次大值(非最大值)
return;
}
标记下传 PsDw
在有了打标记的函数 PsCm
后, 标记下传变得简洁许多, 但是由于分类讨论的情况存在, 这个函数也没有比一般线段树的短.
因为对于没有当前节点最大值子节点, 子节点的最大值受到的修改也只是当前节点的次大值标记, 所以要分类讨论保证每个子节点接受的标记是正确的.
void PsDw(Node *x, const unsigned int &l, const unsigned int &r,
const unsigned int &m) {
unsigned int m1(m + 1); //右子节点的左端点
if (x->L->Mx == x->R->Mx) { //两个儿子都包含当前节点的最大值
PsCm(x->L, x->Tagpls, x->Tagmax, x->HisMxTagpls, x->HisMxTagmax, l,
m); //左儿最大值的两种标记传当前节点最大值对应的标记
PsCm(x->R, x->Tagpls, x->Tagmax, x->HisMxTagpls, x->HisMxTagmax, m1,
r); //右儿最大值的两种标记传当前节点最大值对应的标记
x->Tagpls = x->Tagmax = x->HisMxTagpls = x->HisMxTagmax =
0; //清空标记(标记对自己的影响在父亲传下来的时候已经作用完了)
return;
}
if (x->L->Mx > x->R->Mx) { //最大值在左儿
PsCm(x->L, x->Tagpls, x->Tagmax, x->HisMxTagpls, x->HisMxTagmax, l,
m); //左儿最大值的两种标记传当前节点最大值对应的标记
PsCm(x->R, x->Tagpls, x->Tagpls, x->HisMxTagpls, x->HisMxTagpls, m1,
r); //右儿最大值的两种标记传当前节点非最大值对应的标记
x->Tagpls = x->Tagmax = x->HisMxTagpls = x->HisMxTagmax =
0; //清空标记(标记对自己的影响在父亲传下来的时候已经作用完了)
return;
} //最大值在右儿
PsCm(x->L, x->Tagpls, x->Tagpls, x->HisMxTagpls, x->HisMxTagpls, l,
m); //左儿最大值的标记传当前节点非最大值对应的标记
PsCm(x->R, x->Tagpls, x->Tagmax, x->HisMxTagpls, x->HisMxTagmax, m1,
r); //右儿最大值的两种标记传当前节点最大值对应的标记
x->Tagpls = x->Tagmax = x->HisMxTagpls = x->HisMxTagmax =
0; //清空标记(标记对自己的影响在父亲传下来的时候已经作用完了)
return;
}
区间加减 Addnm
通过 PsCm
打标记, 使其操作和一般和一般线段树无异.
由于区间加减的操作对每个单点雨露均沾 (有点 "各向同性" 的味道), 所以四个标记都打一样的值就可以.
void Addnm(Node *x, unsigned int l, const unsigned int &r) {
if (l >= B && r <= C) { //全包
PsCm(x, D, D, D, D, l,
r); //给当前节点打上修改标记(区间加减修改对最大值和非最大值各向同性)
return;
}
unsigned int m((l + r) >> 1);
PsDw(x, l, r, m); //下传标记以递归左右儿
if (m >= B) { //与左有交
Addnm(x->L, l, m);
}
if (m < C) { //与右有交
Addnm(x->R, m + 1, r);
}
Udt(x, l, r, m); //最后将修改结更新当前
return;
}
区间抹平 Tomin
这是唯一一个递归规则不同, 把复杂度提高了一个 的函数, 因为就算节点区间被操作区间完全包含, 当次大值受波及时, 也要递归其两个子树.
void Tomin(Node *x, unsigned int l, const unsigned int &r) {
if (l >= B && r <= C) { //全包
if (x->Mx <= D) { //都不受影响
return;
}
if (x->SecMx < D) { //节点完全被包含并且仅最大值受影响
PsCm(x, 0, D - x->Mx, 0, D - x->Mx, l, r);
return;
}
} //除此以外都要递归两个子节点
unsigned int m((l + r) >> 1); //递归子节点
PsDw(x, l, r, m);
if (m >= B) {
Tomin(x->L, l, m);
}
if (m < C) {
Tomin(x->R, m + 1, r);
}
Udt(x, l, r, m);
return;
}
区间求和 QrSum
在一层层调用中, 当前节点的各种值已经被传下来的标记更新至最新, 无需再修改, 所以只要被操作区间包含就直接统计, 并且递归完后不再调用 Udt
, 因为查询操作都不会改变已经成为最新状态的节点的值, 所以其他查询操作都不需要 Udt
.
void QrSum(Node *x, unsigned int l, const unsigned int &r) {
if (l >= B && r <= C) { //节点被包含
Lst += x->Sum;
return;
}
unsigned int m((l + r) >> 1);
PsDw(x, l, r, m); //下传标记以递归调用其子树
if (m >= B) { //左边
QrSum(x->L, l, m);
}
if (m < C) { //右边
QrSum(x->R, m + 1, r);
}
return;
}
区间最值 QMaxA
上一个函数改几下就出来了, 答案合并从求和变成取最值, 其余结构一样.
void QMaxA(Node *x, unsigned int l, const unsigned int &r) {
if (l >= B && r <= C) { //全包
Lst = max((long long)x->Mx, Lst);
return;
}
unsigned int m((l + r) >> 1);
PsDw(x, l, r, m);
if (m >= B) {
QMaxA(x->L, l, m);
}
if (m < C) {
QMaxA(x->R, m + 1, r);
}
return;
}
区间历史最值 QMaxB
由于这个函数和上一个函数改变就更小了, 基本上就改了几个变量名, 就不加注释了.
void QMaxB(Node *x, unsigned int l, const unsigned int &r) {
if (l >= B && r <= C) {
Lst = max(Lst, (long long)x->HisMx);
return;
}
unsigned int m((l + r) >> 1);
PsDw(x, l, r, m);
if (m >= B) {
QMaxB(x->L, l, m);
}
if (m < C) {
QMaxB(x->R, m + 1, r);
}
return;
}
主函数 main
别人都说我码风奇特, 但是这个主函数还是比较中规中矩的吧.
int main() {
n = RD();
M = RD();
for (register unsigned int i(1); i <= n; ++i) {
a[i] = RD();
}
memset(N, 0, sizeof(N)); //各种输入 + 初始化
Bld(N, 1, n); //建树, 以N[0]为根, 表示区间 [1,n]
for (register int i(1); i <= M; ++i) { //操作
A = RD();
B = RD();
C = RD();
switch (A) {
case 1: {
D = RD();
Addnm(N, 1, n); //区间加减
break;
}
case 2: {
D = RD();
Tomin(N, 1, n); //区间抹平
break;
}
case 3: {
Lst = 0;
QrSum(N, 1, n); //区间求和
printf("%lld\n", Lst);
break;
}
case 4: {
Lst = -0x3f3f3f3f3f3f3f3f;
QMaxA(N, 1, n); //区间最值
printf("%lld\n", Lst);
break;
}
case 5: {
Lst = -0x3f3f3f3f3f3f3f3f;
QMaxB(N, 1, n); //区间历史最值
printf("%lld\n", Lst);
break;
}
default: {
printf("FYSNB\n");
break;
}
}
}
return 0;
}
总结
这份代码从期末之前就开始写, (2021.01.15 AC Luogu P5494), 放假后已经可以运行, 昨天从上午做到晚上11点, 到今天上午11点半AC已经过去了半个月.
由于一开始没头绪, 看了题解后只理解了前两个标记, 便写了一个两个标记的 分的版本, 并且发帖讨论.
然后理解了剩余两个 Tag
的意义, 自己证明了自己是错的. 开始重构, 然后经历了一晚上加一上午, 终于AC.
毫不夸张地说, 这道题是一道线段树劝退题. 当初学者写完线段树二, 开始做线段树三时, 他们看到 "此题前置知识: AC线段树一" 后欣喜若狂. 然后呢? 看了一会发现没思路, 看题解, 发现自己看题解后也一知半解, 有些意志不坚定的同学便放弃了, 还有些人比着葫芦画瓢, 把题解用自己的码风抄了一遍.
像我这种只认自己的死理, 不愿意放弃自己尚未被证伪的想法的人, 恐怕非常少. 这也是我 CSP2020
爆零的原因. 不愿放弃自己的 julian.cpp
, 然后最后由于心态问题 julian3.in
遗憾离场. 这还是我在好多平台的昵称为 Wild_Donkey
的原因, 像一头倔驴, 朝着自己的方向走.
我不知道这种性格会对我造成什么或好或坏的后果, 但是如果说 OI 这三年半能给我留下最大的财富什么, 那一定是这种锲而不舍的精神和耐心.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
2020-01-31 模拟赛(一)T117903 防疫工作安排
2020-01-31 模拟赛(一)T117901 共享电动车