This commit is contained in:
HuangHai
2025-08-30 18:35:01 +08:00
commit 1f397eca87
6258 changed files with 1487241 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
## [$BZOJ$.$4399$.魔法少女$LJJ$(线段树合并)](https://www.cnblogs.com/SovietPower/p/10386159.html)
TODO
线段树合并

View File

@@ -0,0 +1,5 @@
2
1 2
2
2 1 2 2
4 1 2

View File

@@ -0,0 +1,319 @@
## [$BZOJ4695$. 最假女选手](https://darkbzoj.tk/problem/4695)
### 一、题目描述
给定一个长度为 $N$ 序列,编号从 $1$ 到 $N$ 。要求支持下面几种操作:
1. 给一个区间$[L,R]$ 加上一个数$x$
2. 把一个区间$[L,R]$ 里小于$x$ 的数变成$x$
3. 把一个区间$[L,R]$ 里大于$x$ 的数变成$x$
4. 求区间$[L,R]$的和
5. 求区间$[L,R]$的最大值
6. 求区间$[L,R]$的最小值
**输入**
第一行一个整数 $N$ 表示序列长度。
第二行 $N$ 个整数 $A_i$ 表示初始序列。
第三行一个整数 $M$ 表示操作个数。
接下来 $M$ 行,每行三或四个整数,第一个整数 $Tp$ 表示操作类型,接下来 $L,R,X$ 或 $L,R$ 表述操作数。
$1<=tp<=6,N,M<=5*10^5,|Ai|<=10^8$
$Tp=1$时,$|x|<=1000$
$Tp=2$或$3$时,$|x|<=10^8$
**输出**
对于每个$4,5,6$类型的操作输出一行一个整数表示答案。
**样例输入**
```cpp {.line-numbers}
2
1 2
2
2 1 2 2
4 1 2
```
**样例输出**
```cpp {.line-numbers}
4
```
### 二、解题思路
线段树区间最值操作
[吉老师的$Segment$ $tree$ $Beats!$](https://pan.baidu.com/s/1o7xSSQ2) 中的题。
维护:
- 区间最小值
- 最小值个数
- 严格次小值
- 最大值
- 最大值个数
- 严格次大值
- 区间和
- 区间加和标记
即可。。。
注意: <font color='red' size=4><b>修改区间最小值时要注意其对最大值、严格次大值是否有影响,修改区间最大值时要注意其对最小值、严格次小值的影响</b></font>。
想想都觉得麻烦。。。
时间复杂度 $O(nlog^2n)$
注意本题略微卡常,不能全用$long$ $long$,只有区间和使用$long$ $long$维护。
### 三、实现代码
```cpp {.line-numbers}
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long LL;
//快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
#define ls u << 1
#define rs u << 1 | 1
const int N = 500010;
const int INF = 0x3f3f3f3f;
int n, m;
struct Node {
int l, r;
int mx, cx; //最大值,最大值个数
int sx; //次大值
int mi, ci; //最小值,最小值个数
int si; //次小值
LL sum; //区间和
LL tag; // lazy tag,是不是有未消费掉的add修改标记
} tr[N << 2];
//向上推送统计信息,由左右儿子的统计信息更新父亲u的统计信息
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum; //区间和
//更新最大值系列信息 mx,sx,cx
if (tr[ls].mx == tr[rs].mx) {
tr[u].mx = tr[ls].mx;
tr[u].cx = tr[ls].cx + tr[rs].cx;
tr[u].sx = max(tr[ls].sx, tr[rs].sx);
} else if (tr[ls].mx > tr[rs].mx) {
tr[u].mx = tr[ls].mx;
tr[u].cx = tr[ls].cx;
tr[u].sx = max(tr[ls].sx, tr[rs].mx);
} else if (tr[ls].mx < tr[rs].mx) {
tr[u].mx = tr[rs].mx;
tr[u].cx = tr[rs].cx;
tr[u].sx = max(tr[ls].mx, tr[rs].sx);
}
//更新最小值系列信息 mi,si,ci
if (tr[ls].mi == tr[rs].mi) {
tr[u].mi = tr[ls].mi;
tr[u].si = min(tr[ls].si, tr[rs].si);
tr[u].ci = tr[ls].ci + tr[rs].ci;
} else if (tr[ls].mi > tr[rs].mi) {
tr[u].mi = tr[rs].mi;
tr[u].ci = tr[rs].ci;
tr[u].si = min(tr[ls].mi, tr[rs].si);
} else if (tr[ls].mi < tr[rs].mi) {
tr[u].mi = tr[ls].mi;
tr[u].ci = tr[ls].ci;
tr[u].si = min(tr[ls].si, tr[rs].mi);
}
}
//构建线段树
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].mx = tr[u].mi = tr[u].sum = read(); //最大值,最小值,区间和
tr[u].cx = tr[u].ci = 1; //最大值个数=最小值个数=1
tr[u].sx = -INF; //没有次大值
tr[u].si = INF; //没有次小值
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u);
}
//将区间内所有数字更新为 max(a[i],v)
bool update_max(int u, int v) {
//① 区间最小值都大于v,不需要进入区间
if (tr[u].mi >= v) return true;
//② 如果区间内数字都是一样的
if (tr[u].mx == tr[u].mi) {
tr[u].mx = tr[u].mi = v;
tr[u].cx = tr[u].ci = tr[u].r - tr[u].l + 1;
tr[u].sx = -INF, tr[u].si = INF;
tr[u].sum = (LL)tr[u].cx * v;
return true;
}
//③ 如果区间次小值大于v,只能更新最小值
if (tr[u].si > v) {
tr[u].sum += (LL)tr[u].ci * (v - tr[u].mi);
tr[u].mi = v;
tr[u].sx = max(tr[u].sx, v);
tr[u].mx = max(tr[u].mx, v);
return true;
}
//④ 以上情况都不是,那么讨论不清楚,只能是继续递归左右儿子,在子区间内尝试上面的三种情况更新
return false;
}
//将区间内所有数字更新为 min(a[i],v)
bool update_min(int u, int v) {
//① 区间最大值都小于v,不需要进入区间
if (tr[u].mx <= v) return true;
//② 如果区间内数字都是一样的
if (tr[u].mx == tr[u].mi) {
tr[u].mx = tr[u].mi = v;
tr[u].cx = tr[u].ci = tr[u].r - tr[u].l + 1;
tr[u].sx = -INF, tr[u].si = INF;
tr[u].sum = (LL)tr[u].cx * v;
return true;
}
//③ 如果区间次大值小于v,只能更新最大值
if (tr[u].sx < v) {
tr[u].sum -= (LL)tr[u].cx * (tr[u].mx - v);
tr[u].mx = v;
tr[u].si = min(tr[u].si, v);
tr[u].mi = min(tr[u].mi, v);
return true;
}
//④ 以上情况都不是,那么讨论不清楚,只能是继续递归左右儿子,在子区间内尝试上面的三种情况更新
return false;
}
//整体命中时,区间每个数都要加上v,修改u节点的统计信息,并且传递tag懒标记
void update_add(int u, int v) {
//① 最大值,最小值,次大值,次小值都会受到影响,都要加上v
tr[u].mx += v, tr[u].mi += v, tr[u].sx += v, tr[u].si += v;
//② 区间和也需要进行整体更新
tr[u].sum += (LL)v * (tr[u].r - tr[u].l + 1); //为防止乘法受成的int溢出,需要先强制转换为LL
//③ 区间加的tag懒标记需要标识在此区间上,并没有真的一路更新到叶子节点
tr[u].tag += v;
}
//向下传递懒标记,一般是有新的更新操作,或者,有了查询操作时才这样做
void pushdown(int u) {
if (tr[u].tag) { //如果区间加懒标记不为0,表示存在懒标记,需要通过update_add传递给左右儿子
//主要的目标有两个:1、根据懒标记实时更新左右儿子的统计信息;2、左右儿子继承写上父亲的区间加懒标记
update_add(ls, tr[u].tag), update_add(rs, tr[u].tag);
//传递完毕,父节点区间加懒标记清零
tr[u].tag = 0;
}
//其实本题的懒标记不只是区间加的tag,还有最大值tag和最小值tag,只不过没有显示的写出来
//① 如果父亲的最大值标签信息,比左儿子的最大值小,这就是有问题,需要用tr[u].mx 对左儿子取min操作
if (tr[ls].mx > tr[u].mx) update_min(ls, tr[u].mx);
//② 如果父亲的最大值标签信息,比右儿子的最大值小,这就是有问题,需要用tr[u].mx 对右儿子取min操作
if (tr[rs].mx > tr[u].mx) update_min(rs, tr[u].mx);
//③ 如果父亲的最小值标签信息,比左儿子的最小值大,这就是有问题,需要用tr[u].mi 对左儿子取max操作
if (tr[ls].mi < tr[u].mi) update_max(ls, tr[u].mi);
//④ 如果父亲的最小值标签信息,比右儿子的最小值大,这就是有问题,需要用tr[u].mi 对右儿子取max操作
if (tr[rs].mi < tr[u].mi) update_max(rs, tr[u].mi);
}
//区间加
void modify_add(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l) return; //无交集,直接返回
if (tr[u].l >= l && tr[u].r <= r) { //完全命中
update_add(u, v); //更新统计信息+修改tag懒标记
return;
}
//① 未完全命中,存在部分交集,先将以前的tag传递给左右儿子,再开始递归左右儿子
pushdown(u);
//② 分裂处理左右儿子
modify_add(ls, l, r, v), modify_add(rs, l, r, v);
//③ 因为子节点信息更新了,需要向上汇集统计信息
pushup(u);
}
//更新最大值操作,拆分成了modify_max+update_max两个函数
void modify_max(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l) return; //无交集,直接返回
if (l <= tr[u].l && tr[u].r <= r) //如果区间完整命中
if (update_max(u, v)) return; //需要分类讨论,只有3种情况是可以进行更新的,其它情况只能继续分裂
//分裂
pushdown(u);
modify_max(ls, l, r, v), modify_max(rs, l, r, v);
pushup(u);
}
//更新最小值操作,拆分成了modify_min+update_min两个函数
void modify_min(int u, int l, int r, int v) {
if (l > tr[u].r || r < tr[u].l) return;
if (l <= tr[u].l && tr[u].r <= r)
if (update_min(u, v)) return;
pushdown(u);
modify_min(ls, l, r, v), modify_min(rs, l, r, v);
pushup(u);
}
//区间和
LL query_sum(int u, int l, int r) { //区间和注意用长整型
if (tr[u].l > r || tr[u].r < l) return 0; //无交集,直接返回
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
return query_sum(ls, l, r) + query_sum(rs, l, r);
}
//区间最大值
int query_max(int u, int l, int r) {
if (tr[u].l > r || tr[u].r < l) return -INF; //无交集,返回负无穷,保证结果不会从此区间获益
if (tr[u].l >= l && tr[u].r <= r) return tr[u].mx;
pushdown(u);
return max(query_max(ls, l, r), query_max(rs, l, r));
}
//区间最小值
int query_min(int u, int l, int r) { //无交集,返回正无穷,保证结果不会从此区间获益
if (tr[u].l > r || tr[u].r < l) return INF;
if (tr[u].l >= l && tr[u].r <= r) return tr[u].mi;
pushdown(u);
return min(query_min(ls, l, r), query_min(rs, l, r));
}
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
freopen("BZOJ4695.in", "r", stdin);
#endif
n = read();
build(1, 1, n);
m = read();
while (m--) {
int op = read(), l = read(), r = read(), x;
if (op == 1) x = read(), modify_add(1, l, r, x); //区间加
if (op == 2) x = read(), modify_max(1, l, r, x); //区间修改max运算
if (op == 3) x = read(), modify_min(1, l, r, x); //区间修改min运算
if (op == 4) printf("%lld\n", query_sum(1, l, r)); //查询区间和
if (op == 5) printf("%d\n", query_max(1, l, r)); //查询区间最大值
if (op == 6) printf("%d\n", query_min(1, l, r)); //查询区间最小值
}
return 0;
}
```

View File

@@ -0,0 +1,251 @@
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long LL;
//快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
#define ls u << 1
#define rs u << 1 | 1
const int N = 500010;
const int INF = 0x3f3f3f3f;
int n, m;
struct Node {
int l, r;
int mx, cx; //最大值,最大值个数
int sx; //次大值
int mi, ci; //最小值,最小值个数
int si; //次小值
LL sum; //区间和
LL tag; // lazy tag,是不是有未消费掉的add修改标记
} tr[N << 2];
//向上推送统计信息,由左右儿子的统计信息更新父亲u的统计信息
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum; //区间和
//更新最大值系列信息 mx,sx,cx
if (tr[ls].mx == tr[rs].mx) {
tr[u].mx = tr[ls].mx;
tr[u].cx = tr[ls].cx + tr[rs].cx;
tr[u].sx = max(tr[ls].sx, tr[rs].sx);
} else if (tr[ls].mx > tr[rs].mx) {
tr[u].mx = tr[ls].mx;
tr[u].cx = tr[ls].cx;
tr[u].sx = max(tr[ls].sx, tr[rs].mx);
} else if (tr[ls].mx < tr[rs].mx) {
tr[u].mx = tr[rs].mx;
tr[u].cx = tr[rs].cx;
tr[u].sx = max(tr[ls].mx, tr[rs].sx);
}
//更新最小值系列信息 mi,si,ci
if (tr[ls].mi == tr[rs].mi) {
tr[u].mi = tr[ls].mi;
tr[u].si = min(tr[ls].si, tr[rs].si);
tr[u].ci = tr[ls].ci + tr[rs].ci;
} else if (tr[ls].mi > tr[rs].mi) {
tr[u].mi = tr[rs].mi;
tr[u].ci = tr[rs].ci;
tr[u].si = min(tr[ls].mi, tr[rs].si);
} else if (tr[ls].mi < tr[rs].mi) {
tr[u].mi = tr[ls].mi;
tr[u].ci = tr[ls].ci;
tr[u].si = min(tr[ls].si, tr[rs].mi);
}
}
//构建线段树
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].mx = tr[u].mi = tr[u].sum = read(); //最大值,最小值,区间和
tr[u].cx = tr[u].ci = 1; //最大值个数=最小值个数=1
tr[u].sx = -INF; //没有次大值
tr[u].si = INF; //没有次小值
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u);
}
//将区间内所有数字更新为 max(a[i],v)
bool update_max(int u, int v) {
//① 区间最小值都大于v,不需要进入区间
if (tr[u].mi >= v) return true;
//② 如果区间内数字都是一样的
if (tr[u].mx == tr[u].mi) {
tr[u].mx = tr[u].mi = v;
tr[u].cx = tr[u].ci = tr[u].r - tr[u].l + 1;
tr[u].sx = -INF, tr[u].si = INF;
tr[u].sum = (LL)tr[u].cx * v;
return true;
}
//③ 如果区间次小值大于v,只能更新最小值
if (tr[u].si > v) {
tr[u].sum += (LL)tr[u].ci * (v - tr[u].mi);
tr[u].mi = v;
tr[u].sx = max(tr[u].sx, v);
tr[u].mx = max(tr[u].mx, v);
return true;
}
//④ 以上情况都不是,那么讨论不清楚,只能是继续递归左右儿子,在子区间内尝试上面的三种情况更新
return false;
}
//将区间内所有数字更新为 min(a[i],v)
bool update_min(int u, int v) {
//① 区间最大值都小于v,不需要进入区间
if (tr[u].mx <= v) return true;
//② 如果区间内数字都是一样的
if (tr[u].mx == tr[u].mi) {
tr[u].mx = tr[u].mi = v;
tr[u].cx = tr[u].ci = tr[u].r - tr[u].l + 1;
tr[u].sx = -INF, tr[u].si = INF;
tr[u].sum = (LL)tr[u].cx * v;
return true;
}
//③ 如果区间次大值小于v,只能更新最大值
if (tr[u].sx < v) {
tr[u].sum -= (LL)tr[u].cx * (tr[u].mx - v);
tr[u].mx = v;
tr[u].si = min(tr[u].si, v);
tr[u].mi = min(tr[u].mi, v);
return true;
}
//④ 以上情况都不是,那么讨论不清楚,只能是继续递归左右儿子,在子区间内尝试上面的三种情况更新
return false;
}
//整体命中时,区间每个数都要加上v,修改u节点的统计信息,并且传递tag懒标记
void update_add(int u, int v) {
//① 最大值,最小值,次大值,次小值都会受到影响,都要加上v
tr[u].mx += v, tr[u].mi += v, tr[u].sx += v, tr[u].si += v;
//② 区间和也需要进行整体更新
tr[u].sum += (LL)v * (tr[u].r - tr[u].l + 1); //为防止乘法受成的int溢出,需要先强制转换为LL
//③ 区间加的tag懒标记需要标识在此区间上,并没有真的一路更新到叶子节点
tr[u].tag += v;
}
//向下传递懒标记,一般是有新的更新操作,或者,有了查询操作时才这样做
void pushdown(int u) {
if (tr[u].tag) { //如果区间加懒标记不为0,表示存在懒标记,需要通过update_add传递给左右儿子
//主要的目标有两个:1、根据懒标记实时更新左右儿子的统计信息;2、左右儿子继承写上父亲的区间加懒标记
update_add(ls, tr[u].tag), update_add(rs, tr[u].tag);
//传递完毕,父节点区间加懒标记清零
tr[u].tag = 0;
}
//其实本题的懒标记不只是区间加的tag,还有最大值tag和最小值tag,只不过没有显示的写出来
//① 如果父亲的最大值标签信息,比左儿子的最大值小,这就是有问题,需要用tr[u].mx 对左儿子取min操作
if (tr[ls].mx > tr[u].mx) update_min(ls, tr[u].mx);
//② 如果父亲的最大值标签信息,比右儿子的最大值小,这就是有问题,需要用tr[u].mx 对右儿子取min操作
if (tr[rs].mx > tr[u].mx) update_min(rs, tr[u].mx);
//③ 如果父亲的最小值标签信息,比左儿子的最小值大,这就是有问题,需要用tr[u].mi 对左儿子取max操作
if (tr[ls].mi < tr[u].mi) update_max(ls, tr[u].mi);
//④ 如果父亲的最小值标签信息,比右儿子的最小值大,这就是有问题,需要用tr[u].mi 对右儿子取max操作
if (tr[rs].mi < tr[u].mi) update_max(rs, tr[u].mi);
}
//区间加
void modify_add(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l) return; //无交集,直接返回
if (tr[u].l >= l && tr[u].r <= r) { //完全命中
update_add(u, v); //更新统计信息+修改tag懒标记
return;
}
//① 未完全命中,存在部分交集,先将以前的tag传递给左右儿子,再开始递归左右儿子
pushdown(u);
//② 分裂处理左右儿子
modify_add(ls, l, r, v), modify_add(rs, l, r, v);
//③ 因为子节点信息更新了,需要向上汇集统计信息
pushup(u);
}
//更新最大值操作,拆分成了modify_max+update_max两个函数
void modify_max(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l) return; //无交集,直接返回
if (l <= tr[u].l && tr[u].r <= r) //如果区间完整命中
if (update_max(u, v)) return; //需要分类讨论,只有3种情况是可以进行更新的,其它情况只能继续分裂
//分裂
pushdown(u);
modify_max(ls, l, r, v), modify_max(rs, l, r, v);
pushup(u);
}
//更新最小值操作,拆分成了modify_min+update_min两个函数
void modify_min(int u, int l, int r, int v) {
if (l > tr[u].r || r < tr[u].l) return;
if (l <= tr[u].l && tr[u].r <= r)
if (update_min(u, v)) return;
pushdown(u);
modify_min(ls, l, r, v), modify_min(rs, l, r, v);
pushup(u);
}
//区间和
LL query_sum(int u, int l, int r) { //区间和注意用长整型
if (tr[u].l > r || tr[u].r < l) return 0; //无交集,直接返回
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
return query_sum(ls, l, r) + query_sum(rs, l, r);
}
//区间最大值
int query_max(int u, int l, int r) {
if (tr[u].l > r || tr[u].r < l) return -INF; //无交集,返回负无穷,保证结果不会从此区间获益
if (tr[u].l >= l && tr[u].r <= r) return tr[u].mx;
pushdown(u);
return max(query_max(ls, l, r), query_max(rs, l, r));
}
//区间最小值
int query_min(int u, int l, int r) { //无交集,返回正无穷,保证结果不会从此区间获益
if (tr[u].l > r || tr[u].r < l) return INF;
if (tr[u].l >= l && tr[u].r <= r) return tr[u].mi;
pushdown(u);
return min(query_min(ls, l, r), query_min(rs, l, r));
}
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
freopen("BZOJ4695.in", "r", stdin);
#endif
n = read();
build(1, 1, n);
m = read();
while (m--) {
int op = read(), l = read(), r = read(), x;
if (op == 1) x = read(), modify_add(1, l, r, x); //区间加
if (op == 2) x = read(), modify_max(1, l, r, x); //区间修改max运算
if (op == 3) x = read(), modify_min(1, l, r, x); //区间修改min运算
if (op == 4) printf("%lld\n", query_sum(1, l, r)); //查询区间和
if (op == 5) printf("%d\n", query_max(1, l, r)); //查询区间最大值
if (op == 6) printf("%d\n", query_min(1, l, r)); //查询区间最小值
}
return 0;
}

View File

@@ -0,0 +1,244 @@
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 550000;
const int INF = 0x3f3f3f3f;
#define ls u << 1
#define rs u << 1 | 1
//快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
int n, m;
struct Node {
int l, r;
int mx, cx; //最大值,最大值个数
int sx; //次大值
int mi, ci; //最小值,最小值个数
int si; //次小值
LL sum; //区间和
int add1, add2, add3; //最小值增加,最大值增加,普通值增加
} tr[N << 2];
//向上推送统计信息,由左右儿子的统计信息更新父亲u的统计信息
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum; //区间和
//更新最大值系列信息 mx,sx,cx
if (tr[ls].mx == tr[rs].mx) {
tr[u].mx = tr[ls].mx;
tr[u].cx = tr[ls].cx + tr[rs].cx;
tr[u].sx = max(tr[ls].sx, tr[rs].sx);
} else if (tr[ls].mx > tr[rs].mx) {
tr[u].mx = tr[ls].mx;
tr[u].cx = tr[ls].cx;
tr[u].sx = max(tr[ls].sx, tr[rs].mx);
} else if (tr[ls].mx < tr[rs].mx) {
tr[u].mx = tr[rs].mx;
tr[u].cx = tr[rs].cx;
tr[u].sx = max(tr[ls].mx, tr[rs].sx);
}
//更新最小值系列信息 mi,si,ci
if (tr[ls].mi == tr[rs].mi) {
tr[u].mi = tr[ls].mi;
tr[u].si = min(tr[ls].si, tr[rs].si);
tr[u].ci = tr[ls].ci + tr[rs].ci;
} else if (tr[ls].mi > tr[rs].mi) {
tr[u].mi = tr[rs].mi;
tr[u].ci = tr[rs].ci;
tr[u].si = min(tr[ls].mi, tr[rs].si);
} else if (tr[ls].mi < tr[rs].mi) {
tr[u].mi = tr[ls].mi;
tr[u].ci = tr[ls].ci;
tr[u].si = min(tr[ls].si, tr[rs].mi);
}
}
/*
k1:最小值增量,k2:最大值增量,k3:一般值增量
*/
void update(int u, int k1, int k2, int k3) {
/*
① 更新区间和
如果一个区间的值域很小(比如只有1或2个数),可能会发生一个值既是最大值又是次小值这种情况,也就是发生了数域的重叠.
这种情况要特判,分辨到底该被哪个标记作用。
*/
if (tr[u].mi == tr[u].mx) { // 只有一种值时,最大值等于最小值
// if (k1 == k3) k1 = k2;
// if (k2 == k3) k2 = k1;
// 不应被其他值的标记作用
if (k1 == k3)
k1 = k2;
else
k2 = k1;
tr[u].sum += (LL)k1 * tr[u].ci;
} else
//整体增量=最小值增量*个数+最大值增量*个数+普通值增量*个数
tr[u].sum += (LL)k1 * tr[u].ci + (LL)k2 * tr[u].cx + (LL)k3 * (tr[u].r - tr[u].l + 1 - tr[u].ci - tr[u].cx);
//② 更新次小
if (tr[u].si == tr[u].mx) //次小等于最大
tr[u].si += k2;
else if (tr[u].si != INF) //次小存在
tr[u].si += k3;
//③ 更新次大
if (tr[u].sx == tr[u].mi) //次大等于最小
tr[u].sx += k1;
else if (tr[u].sx != -INF) //次大存在
tr[u].sx += k3;
//④ 更新最小值,最大值
tr[u].mi += k1, tr[u].mx += k2;
//⑤ 向左右儿子传递懒标记add1,add2,add3
tr[u].add1 += k1, tr[u].add2 += k2, tr[u].add3 += k3;
}
//向下传递懒标记,一般是有新的更新操作,或者,有了查询操作时才这样做
void pushdown(int u) {
int mi = min(tr[ls].mi, tr[rs].mi);
int mx = max(tr[ls].mx, tr[rs].mx);
/*
k1:最小值增量,k2:最大值增量,k3:一般值增量
有三个lazy add标签,需要注意互相之间的影响关系:
① 修改区间最小值时要注意其对最大值、严格次大值的影响
② 修改区间最大值时要注意其对最小值、严格次小值的影响
*/
int k1, k2, k3 = tr[u].add3; //一般值没啥说道,直接传递给左右儿子就行
tr[ls].mi == mi ? k1 = tr[u].add1 : k1 = tr[u].add3; //左儿子中有最小值,k1=tr[u].add1,否则k1=tr[u].add3
tr[ls].mx == mx ? k2 = tr[u].add2 : k2 = tr[u].add3; //左儿子中有最大值,k2=tr[u].add2,否则k1=tr[u].add3
update(ls, k1, k2, k3); //标签纠正好了,可以更新左儿子了
//复读机
tr[rs].mi == mi ? k1 = tr[u].add1 : k1 = tr[u].add3; //右儿子中有最小值,k1=tr[u].add1,否则k1=tr[u].add3
tr[rs].mx == mx ? k2 = tr[u].add2 : k2 = tr[u].add3; //右儿子中有最大值,k2=tr[u].add2,否则k1=tr[u].add3
update(rs, k1, k2, k3); //标签纠正好了,可以更新右儿子了
//传递完了add lzay tag,重置为0,表示没的传了
tr[u].add1 = tr[u].add2 = tr[u].add3 = 0;
}
//构建线段树
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].mx = tr[u].mi = tr[u].sum = read(); //最大值,最小值,区间和
tr[u].cx = tr[u].ci = 1; //最大值个数=最小值个数=1
tr[u].sx = -INF; //没有次大值
tr[u].si = INF; //没有次小值
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u);
}
//区间加
void modify_add(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l) return; //无交集,直接返回
if (tr[u].l >= l && tr[u].r <= r) { //区间完整命中
update(u, v, v, v); //更新统计信息,最大值,最小值,一般值 都是一样的需要+v
return;
}
//① 未完全命中,存在部分交集,先将以前的tag传递给左右儿子,再开始递归左右儿子
pushdown(u);
//② 分裂处理左右儿子
modify_add(ls, l, r, v), modify_add(rs, l, r, v);
//③ 因为子节点信息更新了,需要向上汇集统计信息
pushup(u);
}
//区间修改最大值
void modify_max(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l || v <= tr[u].mi) return; //如果区间无交集,或者,要更新的最大值还没有人家区间的最小值大,没有更新的必要
if (l <= tr[u].l && tr[u].r <= r && v < tr[u].si) { //如果区间完全命中,并且,v小于区间次小值,那么只能更新最小值
update(u, v - tr[u].mi, 0, 0);
//最小值被更新的大小=为add(v-tr[u].mi),如此,成功将设置为最小值操作转化为加法标签操作
return;
}
//① 未完全命中,存在部分交集,先将以前的tag传递给左右儿子,再开始递归左右儿子
pushdown(u);
//② 分裂处理左右儿子
modify_max(ls, l, r, v), modify_max(rs, l, r, v);
//③ 因为子节点信息更新了,需要向上汇集统计信息
pushup(u);
}
//区间修改最小值
void modify_min(int u, int l, int r, int v) {
if (tr[u].l > r || tr[u].r < l || v >= tr[u].mx) return; //如果区间无交集,或者,要更新的最小值还比人家区间的最大值还大,没有更新的必要
if (l <= tr[u].l && tr[u].r <= r && v > tr[u].sx) { //如果区间完全命中,并且,v大于区间次大值,那么只能更新最大值
update(u, 0, v - tr[u].mx, 0);
//最大值被更新的大小=为add(v-tr[u].mx),如此,成功将设置为最大值操作转化为加法标签操作
return;
}
//① 未完全命中,存在部分交集,先将以前的tag传递给左右儿子,再开始递归左右儿子
pushdown(u);
//② 分裂处理左右儿子
modify_min(ls, l, r, v), modify_min(rs, l, r, v);
//③ 因为子节点信息更新了,需要向上汇集统计信息
pushup(u);
}
//查询区间和
LL query_sum(int u, int l, int r) {
if (tr[u].l > r || tr[u].r < l) return 0;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
return query_sum(ls, l, r) + query_sum(rs, l, r);
}
//区间最大值
int query_max(int u, int l, int r) {
if (tr[u].l > r || tr[u].r < l) return -INF;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].mx;
pushdown(u);
return max(query_max(ls, l, r), query_max(rs, l, r));
}
//区间最小值
int query_min(int u, int l, int r) {
if (tr[u].l > r || tr[u].r < l) return INF;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].mi;
pushdown(u);
return min(query_min(ls, l, r), query_min(rs, l, r));
}
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
freopen("BZOJ4695.in", "r", stdin);
#endif
n = read();
build(1, 1, n);
m = read();
while (m--) {
int op = read(), l = read(), r = read(), x;
if (op == 1) x = read(), modify_add(1, l, r, x); //区间加
if (op == 2) x = read(), modify_max(1, l, r, x); //区间修改max运算
if (op == 3) x = read(), modify_min(1, l, r, x); //区间修改min运算
if (op == 4) printf("%lld\n", query_sum(1, l, r)); //查询区间和
if (op == 5) printf("%d\n", query_max(1, l, r)); //查询区间最大值
if (op == 6) printf("%d\n", query_min(1, l, r)); //查询区间最小值
}
return 0;
}

View File

@@ -0,0 +1,141 @@
#include <iostream>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const int N = 1e6 + 10;
#define ls u << 1
#define rs u << 1 | 1
//快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
struct Node {
int l, r, mx, se, cnt; // 区间最大值mx, 区间次大值se, 区间最大值个数cnt
LL sum; //区间和
} tr[N << 2];
//向上一级推送统计信息
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum;
tr[u].mx = max(tr[ls].mx, tr[rs].mx);
if (tr[ls].mx == tr[rs].mx) { //左右儿子最大值样等
tr[u].cnt = tr[ls].cnt + tr[rs].cnt; //父亲的最大值个数=左儿子最大值个数+右儿子最大值个数
tr[u].se = max(tr[ls].se, tr[rs].se); //次大的,就需要讨论一下左右儿子谁的次大比较大
}
if (tr[ls].mx > tr[rs].mx) { //如果左儿子最大值大于右儿子最大值
tr[u].cnt = tr[ls].cnt; //记录左儿子最大值个数
tr[u].se = max(tr[ls].se, tr[rs].mx); //次大变成左儿子次大值,与右儿子最大值PK
}
if (tr[ls].mx < tr[rs].mx) {
tr[u].cnt = tr[rs].cnt;
tr[u].se = max(tr[rs].se, tr[ls].mx);
}
}
//构建线段树
void build(int u, int l, int r) {
// 每个都认真初始化,全部赋值一遍最稳妥
// 此题卡时间卡的比较严重,如果不用下面的方法,每次memset(tr,0,sizeof(tr));就会TLE
tr[u].l = l, tr[u].r = r, tr[u].mx = tr[u].cnt = tr[u].sum = 0;
tr[u].se = -INF; //初始时没有次大值,最小值是0,所以设置成-INF就肯定小于最小值了
if (l == r) {
tr[u].sum = tr[u].mx = read();
tr[u].cnt = 1;
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u);
}
//将modify拆分成两个,独立出来
void update_min(int u, int v) {
if (tr[u].mx <= v) return; //最大值都没有k大,k就是无用的东西
tr[u].sum += ((LL)v - tr[u].mx) * tr[u].cnt; // 区间整体命中,并且比最大值小,能修改的只有最大值
tr[u].mx = v; // 最大值修改为k
}
//向下一级传递懒标记tag标记,此时的懒标记就是tr[u].mx也就是区间最大值
void pushdown(int u) {
update_min(ls, tr[u].mx), update_min(rs, tr[u].mx);
}
void modify_min(int u, int l, int r, int v) {
// ①与修改区间与当前区间无次,或,k>=tr[u].mx 时,没有修改的必要
if (r < tr[u].l || l > tr[u].r || v >= tr[u].mx) return;
// ②整个区间命中,不用再分裂,并且,tr[u].se<k<tr[u].mx ,update(u,k)将只改最大
if (l <= tr[u].l && tr[u].r <= r && tr[u].se < v) {
update_min(u, v);
return;
}
//③ k <=tr[u].se 时,无法实现只对最大值的修改,只能继续分裂,直到区间可以被上面两种条件覆盖到
pushdown(u); //先下传lazy懒标记,防止有上次的累计懒标记没有处理,造成标记错乱或丢失
//递归左右儿子
modify_min(ls, l, r, v), modify_min(rs, l, r, v);
//叶子节点修改后,别忘了再上传统计信息,比如什么最大值个数,次大值是谁啥的,上面可都没管,但最终修改后要更新一次
pushup(u);
}
//普通线段树的区间最大值
int query_max(int u, int l, int r) {
if (r < tr[u].l || l > tr[u].r) return -INF;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].mx;
pushdown(u);
return max(query_max(ls, l, r), query_max(rs, l, r));
}
//普通线段树的区间和
LL query_sum(int u, int l, int r) {
if (r < tr[u].l || l > tr[u].r) return 0;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum; //完全在查询区间内,直接返回tr[u].sum
pushdown(u);
return query_sum(ls, l, r) + query_sum(rs, l, r);
}
void solve() {
int n = read(), m = read();
//构建线段树
build(1, 1, n);
while (m--) {
int op = read(), l = read(), r = read();
if (op == 0) {
int v = read();
modify_min(1, l, r, v); //将区间[l,r]中a[i](i∈[l,r]),与x取min,最后,将整个区间修改为min(x,a[i])
}
if (op == 1) printf("%d\n", query_max(1, l, r)); //查询区间最大值
if (op == 2) printf("%lld\n", query_sum(1, l, r)); // 查询区间和
}
}
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
freopen("HDU5306.in", "r", stdin);
#endif
int T = read();
for (int i = 1; i <= T; i++) solve(); //多组测试数据,提取solve函数,然后执行T次是一个好方法
return 0;
}

View File

@@ -0,0 +1,10 @@
1
5 5
1 2 3 4 5
1 1 5
2 1 5
0 3 5 3
1 1 5
2 1 5
http://acm.hdu.edu.cn/showproblem.php?pid=5306

View File

@@ -0,0 +1,168 @@
## [$HDU$ $5306$ $Gorgeous$ $Sequence$](http://acm.hdu.edu.cn/showproblem.php?pid=5306)
#### 标签:
**区间最值操作**,**吉司机线段树**,**简单模板题**
### 一、题目描述
现在有这样的一个问题:
你有一个长度为$n$($n≤1e6$)的序列,你将会进行$m$($m≤1e6$)次操作,每次操作属于下列三种形式之一:
- $0$ $l$ $r$ $x$ ,表示对于每一个$i(l≤i≤r)$,进行$a_i=min(a_i,x)$操作。
- $1$ $l$ $r$,询问区间$[l,r]$内当前的区间最大值
- $2$ $l$ $r$,询问区间$[l,r]$内的区间和,即 $\displaystyle \sum_{i=l}^ra_i$
### 二、解题思路
对于这样的问题(包括其各种加强版),$jls$在$2016$的国家集训队论文集中给出了如下的解法,即$segment$ $tree$ $beats$,一种采用势能思想的线段树。
**吉老师论文:**
<center><img src='https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWFnZXMyMDE1LmNuYmxvZ3MuY29tL2Jsb2cvODkyNzU4LzIwMTcwMy84OTI3NTgtMjAxNzAzMjkyMTQ3NDcxNzAtMTc3Mjk4Ni5wbmc?x-oss-process=image/format,png'></center>
<center><img src='https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWFnZXMyMDE1LmNuYmxvZ3MuY29tL2Jsb2cvODkyNzU4LzIwMTcwMy84OTI3NTgtMjAxNzAzMjkyMTQ4MTczMjYtMTEzNzU2MzI5NC5wbmc?x-oss-process=image/format,png'></center>
### 三、实现代码
```cpp {.line-numbers}
#include <iostream>
using namespace std;
typedef long long LL;
const int INF = 0x3f3f3f3f;
const int N = 1e6 + 10;
#define ls u << 1
#define rs u << 1 | 1
//快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
struct Node {
int l, r, mx, se, cnt; // 区间最大值mx, 区间次大值se, 区间最大值个数cnt
LL sum; //区间和
} tr[N << 2];
//向上一级推送统计信息
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum;
tr[u].mx = max(tr[ls].mx, tr[rs].mx);
if (tr[ls].mx == tr[rs].mx) { //左右儿子最大值样等
tr[u].cnt = tr[ls].cnt + tr[rs].cnt; //父亲的最大值个数=左儿子最大值个数+右儿子最大值个数
tr[u].se = max(tr[ls].se, tr[rs].se); //次大的,就需要讨论一下左右儿子谁的次大比较大
}
if (tr[ls].mx > tr[rs].mx) { //如果左儿子最大值大于右儿子最大值
tr[u].cnt = tr[ls].cnt; //记录左儿子最大值个数
tr[u].se = max(tr[ls].se, tr[rs].mx); //次大变成左儿子次大值,与右儿子最大值PK
}
if (tr[ls].mx < tr[rs].mx) {
tr[u].cnt = tr[rs].cnt;
tr[u].se = max(tr[rs].se, tr[ls].mx);
}
}
//构建线段树
void build(int u, int l, int r) {
// 每个都认真初始化,全部赋值一遍最稳妥
// 此题卡时间卡的比较严重,如果不用下面的方法,每次memset(tr,0,sizeof(tr));就会TLE
tr[u].l = l, tr[u].r = r, tr[u].mx = tr[u].cnt = tr[u].sum = 0;
tr[u].se = -INF; //初始时没有次大值,最小值是0,所以设置成-INF就肯定小于最小值了
if (l == r) {
tr[u].sum = tr[u].mx = read();
tr[u].cnt = 1;
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u);
}
//将modify拆分成两个,独立出来
void update_min(int u, int v) {
if (tr[u].mx <= v) return; //最大值都没有k大,k就是无用的东西
tr[u].sum += ((LL)v - tr[u].mx) * tr[u].cnt; // 区间整体命中,并且比最大值小,能修改的只有最大值
tr[u].mx = v; // 最大值修改为k
}
//向下一级传递懒标记tag标记,此时的懒标记就是tr[u].mx也就是区间最大值
void pushdown(int u) {
update_min(ls, tr[u].mx), update_min(rs, tr[u].mx);
}
void modify_min(int u, int l, int r, int v) {
// ①与修改区间与当前区间无次,或,k>=tr[u].mx 时,没有修改的必要
if (r < tr[u].l || l > tr[u].r || v >= tr[u].mx) return;
// ②整个区间命中,不用再分裂,并且,tr[u].se<k<tr[u].mx ,update(u,k)将只改最大
if (l <= tr[u].l && tr[u].r <= r && tr[u].se < v) {
update_min(u, v);
return;
}
//③ k <=tr[u].se 时,无法实现只对最大值的修改,只能继续分裂,直到区间可以被上面两种条件覆盖到
pushdown(u); //先下传lazy懒标记,防止有上次的累计懒标记没有处理,造成标记错乱或丢失
//递归左右儿子
modify_min(ls, l, r, v), modify_min(rs, l, r, v);
//叶子节点修改后,别忘了再上传统计信息,比如什么最大值个数,次大值是谁啥的,上面可都没管,但最终修改后要更新一次
pushup(u);
}
//普通线段树的区间最大值
int query_max(int u, int l, int r) {
if (r < tr[u].l || l > tr[u].r) return -INF;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].mx;
pushdown(u);
return max(query_max(ls, l, r), query_max(rs, l, r));
}
//普通线段树的区间和
LL query_sum(int u, int l, int r) {
if (r < tr[u].l || l > tr[u].r) return 0;
if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum; //完全在查询区间内,直接返回tr[u].sum
pushdown(u);
return query_sum(ls, l, r) + query_sum(rs, l, r);
}
void solve() {
int n = read(), m = read();
//构建线段树
build(1, 1, n);
while (m--) {
int op = read(), l = read(), r = read();
if (op == 0) {
int v = read();
modify_min(1, l, r, v); //将区间[l,r]中a[i](i∈[l,r]),与x取min,最后,将整个区间修改为min(x,a[i])
}
if (op == 1) printf("%d\n", query_max(1, l, r)); //查询区间最大值
if (op == 2) printf("%lld\n", query_sum(1, l, r)); // 查询区间和
}
}
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
freopen("HDU5306.in", "r", stdin);
#endif
int T = read();
for (int i = 1; i <= T; i++) solve(); //多组测试数据,提取solve函数,然后执行T次是一个好方法
return 0;
}
```

View File

@@ -0,0 +1,20 @@
5 6
1 2 3 4 5
3 2 5
1 1 3 3
4 2 4
2 3 4 1
5 1 5
3 1 4
题目地址:
https://www.luogu.com.cn/problem/P6242
题解:
https://www.luogu.com.cn/blog/Sqrtree/solution-p6242
输出:
14
6
6
11

View File

@@ -0,0 +1,309 @@
##[$P6242$ 【模板】线段树 (吉司机线段树) $3$](https://www.luogu.com.cn/problem/P6242)
未能AC掉,回头再看吧 2023.01.10
https://www.cnblogs.com/ZZM-248/p/16995016.html?share_token=64b71fe6-b62f-4adb-942a-7d135323116b
https://www.luogu.com.cn/blog/Sqrtree/solution-p6242
[吉老师$PPT$下载](https://pan.baidu.com/s/1_QkSm2C0N-sqAqiBdpPkGg?pwd=36th)
[参考题解I](https://www.luogu.com.cn/blog/Sqrtree/solution-p6242)
[参考题解II](https://blog.csdn.net/Miniqwq/article/details/119618123)
[参考题解III](https://www.luogu.com.cn/blog/Hakurei-Reimu/seg-beats)
[视频资源](https://www.bilibili.com/video/BV1E7411A7D7)
[$HDU$ $5306$ $Gorgeous$ $Sequence$](http://acm.hdu.edu.cn/showproblem.php?pid=5306)
[有趣的线段树维护——吉老师线段树学习笔记](https://www.watertomato.com/%e6%9c%89%e8%b6%a3%e7%9a%84%e7%ba%bf%e6%ae%b5%e6%a0%91%e7%bb%b4%e6%8a%a4-%e5%90%89%e8%80%81%e5%b8%88%e7%ba%bf%e6%ae%b5%e6%a0%91%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0/)
### 一、题目简介
给你一个长度为 $n$ 的序列 $a$。令序列 $b=a$。
现在让你进行 $m$ 次操作,分为 $5$ 种:
`1 l r k`:将序列 $a$ 中区间 $[l,r]$ 的数加上 $k$
`2 l r v`:将序列 $a$ 中区间 $[l,r]$ 的数对 $v$ 取最小值
`3 l r`:求序列 $a$ 中区间 $[l,r]$ 的数的和
`4 l r`:求序列 $a$ 中区间 $[l,r]$ 的数的最大值
`5 l r`:求序列 $b$ 中区间 $[l,r]$ 的数的最大值
每次操作后令 $b_{i}=\max(b_{i},a_{i})$。
### 二、思路
懒标记:
由来和定义:
众所周知,线段树可以支持O(logn)的时间复杂度的区间查询,但如果要进行区间修改的话会导致所有区间的在该区间内的节点的子树下的子节点的信息都要被修改,时间复杂度度为O(N).
如果之后却根本没有用到修改后的区间的信息则前面的修改操作都是浪费时间的,所以这里我们要给当前节点的区间都在修改区间内的节点都加上一个懒标记,并完成当前节点的修改,而至于该节点下的子树的修改则是留到下一次查询或者区间修改的时候将懒标记下传。
以区间增加的懒标记为例
前置知识:线段树。以及会写操作 $2$ 以外的所有操作。
[吉司机线段树模版题](https://blog.csdn.net/ZHUYINGYE_123456/article/details/126449970),考虑用线段树维护。
先引入一下 $3$ 个概念:
- **$a_i$ 的历史最(小/大)值**:$a_{i}$ 存放过的最(小/大)的数
- **区间最(小/大)值操作**:将序列 $a$ 中区间 $[l,r]$ 中的数 $a_{i}$ 变为 $\min(a_i,x)$ 或 $\max(a_i,x)$,$i \in [l,r]$
- **严格次大值**:第二大的值,必须小于最大的
显然在这题中,
① $b_i$ 就是 $a_i$ 的 **历史最大值**
② 操作 $2$ 就是 **区间最小值操作**
③ 操作 $5$ 就是 **区间历史最大值**
本题我们可以发现,$B_i=max(B_i,A_i)$,因此$B$数组维护的是$A$数组的历史最大值,在线段树建树的时候,$A$数组和$B$数组就可以 **共用一颗线段树**
<!-- 让表格居中显示的风格 -->
<style>
.center
{
width: auto;
display: table;
margin-left: auto;
margin-right: auto;
}
</style>
<div class="center">
| **操作** | **描述** |
| ---- | ---- |
| 1 | 区间加 |
| 2 | 区间修改为最小值 |
| 3 | 询问区间和 |
| 4 | 查询区间最大值 |
| 5 | 查询区间历史最大值 |
</div>
#### 线段树结构体
- $l,r$:区间的左右端点
- $mx$:区间内的最大值
- $mxb$:区间内的历史最大值
- $cnt$:区间内最大值的个数
- $se$:区间内的严格次大值
- $sum$:区间内元素的和
- $add1$:$A$数组中 **最大值的懒标记**
- $add2$:$B$数组中 **最大值的懒标记**
- $add3$:$A$数组中 **除最大值外所有元素的懒标记**
- $add4$:$B$数组中 **除最大值外所有元素的懒标记**
```cpp {.line-numbers}
struct Node { // am为当前最大值,bm为历史最大值,se为严格次大值,cnt为区间内最大值的个数
int l, r, am, bm, cnt, se;
LL sum;
// add1为A[]中最大值加法加的最多的一次懒标记,add3为A[]中除最大值外所有元素加的最多的一次懒标记
// add2为B[]中最大值加法加的最多的一次懒标记,add4为B[]中除最大值外所有元素加的最多的一次懒标记
int add1, add2, add3, add4;
} tr[N << 2];
```
#### 建树
递归建树,当递归到了叶子节点时,初始化$sum,am,bm$都为$A$数组中的元素,$se$初始化为$INF$,这样能保证本题所有的数都比它大,$cnt$初始化为$1$,因为这个区间长度为$1$,只有一个最大值。
```cpp {.line-numbers}
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].sum = tr[u].am = tr[u].bm = read(); //初始化
tr[u].se = INF; //次大值初始化
tr[u].cnt = 1; //区间最大值的数量为1
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u);
}
```
#### 向上更新
每次通过左右儿子来维护父节点的最大值和区间和
**父区间最大值出现的次数**:
- 当左右区间最大值相等时,父区间的最大值出现次数为两个子区间相加
- 否则,父区间最大值出现的次数为两个子区间中区间最大值大的那个区间的最大值出现次数
**父区间的严格次大值**:
- 当左右区间最大值相等时,父区间的次大值为两个子区间次大值大的那一个
- 否则,父区间次大值等于最大值小的那个区间的最大值和另一个区间的次大值取一个$max$
```cpp {.line-numbers}
void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum; //更新区间和
tr[u].am = max(tr[ls].am, tr[rs].am); //更新区间最大值
tr[u].bm = max(tr[ls].bm, tr[rs].bm); //更新区间历史最大值
//当左区间最大值与右区间最大值相等时
if (tr[ls].am == tr[rs].am) { //父区间次大值等于两个子区间次大值中大的一个
tr[u].se = max(tr[ls].se, tr[rs].se);
//父区间最大值个数等于两个子区间的和
tr[u].cnt = tr[ls].cnt + tr[rs].cnt;
} //左区间最大值大于右区间最大值
else if (tr[ls].am > tr[rs].am) { //父区间次大值等于max(左区间次大值,右区间最大值)
tr[u].se = max(tr[ls].se, tr[rs].am);
//父区间最大值个数等于左区间最大值个数
tr[u].cnt = tr[ls].cnt;
} //左区间最大值小于右区间最大值
else { //父区间次大值等于max(左区间最大值,右区间次大值)
tr[u].se = max(tr[ls].am, tr[rs].se);
//父区间最大值个数等于右区间最大值个数
tr[u].cnt = tr[rs].cnt;
}
}
```
#### 懒标记下放
为了方便理解,**将懒标记下放操作分为两个函数**,`update()`其实是`pushdown()`的子函数
<font color='red' size=4><b>$update()$函数</b></font>
**① 更新区间和**:把区间里的 **最大值个数** 乘上 $k1$,**其它数的个数** 乘上 $k3$,它们的和就是区间和新增的部分
**② 更新区间次大值**:如果区间不同值个数大于$1$,那么加上 $k3$
**③ 更新$4$个懒标记**
```cpp {.line-numbers}
/*
它爹手中有信息想要下传,它有信息包括:
*/
inline void update(int u, int k1, int k2, int k3, int k4) {
//区间和:把区间里最大值个数乘上add1,其他数的个数乘上add3,它们的和就是区间和新增部分
tr[u].sum += ((LL)k1 * tr[u].cnt) + ((LL)k3 * (tr[u].r - tr[u].l + 1 - tr[u].cnt));
tr[u].bm = max(tr[u].bm, tr[u].am + k2);
tr[u].am += k1;
//区间次大值:如果区间不同值个数不为一,那么加上add3
if (tr[u].se != INF) tr[u].se += k3;
//更新懒标记
tr[u].add2 = max(tr[u].add2, tr[u].add1 + k2);
tr[u].add4 = max(tr[u].add4, tr[u].add3 + k4);
tr[u].add1 += k1, tr[u].add3 += k3;
}
```
<font color='red' size=4><b>$pushdown()$函数</b></font>
对于每个子区间,如果它的最大值比另一区间的最大值大,那么父区间就要更新最大值,否则更新非最大值(注意,最大值需要先记录下来,不能在判断时比较两个子区间最大值大小)。
懒标记清零。
```cpp {.line-numbers}
inline void pushdown(int u) {
//对于左右子区间,如果其中一个区间的最大值比另一区间最大值大,那么父区间就要更新最大值,否则更新非最大值
int fmax = max(tr[ls].am, tr[rs].am);//取左右子树的最大值
if (tr[ls].am == fmax)//如果左子树中有最大值
update(ls, tr[u].add1, tr[u].add2, tr[u].add3, tr[u].add4);//更新最大值标签
else//如果左子树中没有最大值
update(ls, tr[u].add3, tr[u].add4, tr[u].add3, tr[u].add4);//更新非最大值标签
//复读机
if (tr[rs].am == fmax)
update(rs, tr[u].add1, tr[u].add2, tr[u].add3, tr[u].add4);
else
update(rs, tr[u].add3, tr[u].add4, tr[u].add3, tr[u].add4);
//更新后懒标记清零
tr[u].add1 = tr[u].add2 = tr[u].add3 = tr[u].add4 = 0;
}
```
#### 区间加
如果区间在待更新的范围内,那么把所有数都加上 `d` ,即 `update(u, d, d, d, d)`。
```cpp {.line-numbers}
void add(int u, int l, int r, int d) {
if (tr[u].l >= l && tr[u].r <= r) {
update(u, d, d, d, d);
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) add(ls, l, r, d);
if (r > mid) add(rs, l, r, d);
pushup(u);
}
```
#### 区间修改为最小值
如果区间最大值都比 d 小,那么无需更新,直接 return。
否则,如果s e ≤ d ≤ a m se≤d≤amse≤d≤am,只需更新am,把update()中的k1和k2设置为d - tr[u].am(因为update()是加法操作,我们只需加上负数就等于减去这个数)。
```cpp {.line-numbers}
void change_min(int u, int l, int r, int d) {
if (tr[u].am <= d) return;
if (tr[u].l >= l && tr[u].r <= r && tr[u].se < d) {
update(u, d - tr[u].am, d - tr[u].am, 0, 0);
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) change_min(ls, l, r, d);
if (r > mid) change_min(rs, l, r, d);
pushup(u);
}
```
#### 询问区间和
注意开long long
```cpp {.line-numbers}
LL querysum(int u, int l, int r) { //查询区间和
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
LL res = 0;
if (l <= mid) res = querysum(ls, l, r);
if (r > mid) res += querysum(rs, l, r);
return res;
}
```
#### 查询区间最大值
```cpp {.line-numbers}
int query_am(int u, int l, int r) { //查询max_a[i]
if (tr[u].l >= l && tr[u].r <= r) return tr[u].am;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
int res = INF;
if (l <= mid) res = max(res, query_am(ls, l, r));
if (r > mid) res = max(res, query_am(rs, l, r));
return res;
}
```
#### 查询历史最大值
```cpp {.line-numbers}
int query_bm(int u, int l, int r) { //查询max_b[i]
if (tr[u].l >= l && tr[u].r <= r) return tr[u].bm;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
int res = INF;
if (l <= mid) res = max(res, query_bm(ls, l, r));
if (r > mid) res = max(res, query_bm(rs, l, r));
return res;
}
```
https://blog.csdn.net/weixin_51053573/article/details/125821279

View File

@@ -0,0 +1,227 @@
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 500010;
const int INF = 0x3f3f3f3f;
#define ls u << 1
#define rs ls | 1
//快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
int n, m;
struct Node { // am为当前最大值,bm为历史最大值,se为严格次大值,cnt为区间内最大值的个数
int l, r; //区间范围
int ma, mb; //当前区间最大值,历史区间最大值
int cnt, se; //最大值个数,次大值
LL sum; //区间和
int add1; //当前区间最大值的加标记
int add2; //当前区间非最大值的加标记
int add3; //当前区间最大值历史上加的最多的那次的加标记
int add4; //当前区间非最大值的历史上加的最多的那次的加标记
} tr[N << 2];
//向上更新统计信息
void pushup(int u) {
//目标:更新u管辖范围内区间和,当前区间最大值,历史区间最大值
tr[u].sum = tr[ls].sum + tr[rs].sum;
tr[u].ma = max(tr[ls].ma, tr[rs].ma);
tr[u].mb = max(tr[ls].mb, tr[rs].mb);
//目标:更新u管辖范围内次大值,最大值个数
//根据左右儿子的最大值分类讨论,共三种情况:
if (tr[ls].ma == tr[rs].ma) { //左区间最大值与右区间最大值相等
tr[u].se = max(tr[ls].se, tr[rs].se); //父区间次大值等于两个子区间次大值中大的一个
tr[u].cnt = tr[ls].cnt + tr[rs].cnt; //父区间最大值个数等于两个子区间的和
} //左区间最大值大于右区间最大值
if (tr[ls].ma > tr[rs].ma) { //不解释了
tr[u].se = max(tr[ls].se, tr[rs].ma);
tr[u].cnt = tr[ls].cnt;
}
if (tr[ls].ma < tr[rs].ma) {
tr[u].se = max(tr[ls].ma, tr[rs].se);
tr[u].cnt = tr[rs].cnt;
}
}
//构建线段树
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].sum = tr[u].ma = tr[u].mb = read(); //区间和=当前区间最大值=历史区间最大值=节点值
tr[u].se = -INF; //次大值(只有一个节点没有次大值,设置负无穷)
tr[u].cnt = 1; //区间最大值数量=1
return;
}
int mid = (l + r) >> 1;
build(ls, l, mid), build(rs, mid + 1, r);
pushup(u); //向上更新统计信息
}
/*
*/
inline void update(int u, int k1, int k2, int k3, int k4) {
//区间和的变化
tr[u].sum += ((LL)k1 * tr[u].cnt) + ((LL)k3 * (tr[u].r - tr[u].l + 1 - tr[u].cnt));
//更新历史最大值
tr[u].mb = max(tr[u].mb, tr[u].ma + k2); //什么意思?
// a数组中,最大值被加了个k1
tr[u].ma += k1;
//区间次大值:如果区间不同值个数不为1,那么加上k3
if (tr[u].se != -INF) tr[u].se += k3;
//更新懒标记
//(1) 2和4 需要把取一下max再考虑下传
tr[u].add3 = max(tr[u].add3, tr[u].add1 + k2); //这两句话怎么理解?
tr[u].add4 = max(tr[u].add4, tr[u].add2 + k4); //这两句话怎么理解?
//(2) 1和3直接下传
tr[u].add1 += k1, tr[u].add2 += k3;
}
/*
懒标记:
线段树可以支持O(logn)的时间复杂度的区间查询,但如果要进行区间修改的话会导致所有区间的在该区间内的节点的子树下
的子节点的信息都要被修改,时间复杂度度为O(N).
如果之后却根本没有用到修改后的区间的信息则前面的修改操作都是浪费时间的,所以这里我们要给当前节点的区间都在修改
区间内的节点都加上一个懒标记,并完成当前节点的修改,而至于该节点下的子树的修改则是留到下一次查询或者区间修改的
时候将懒标记下传。
回到本题:
(1)pushdown要传递什么?
答:要将add1,add2,add3,add4四个懒标记传递给左右儿子
当前版本最大值加标记,当前版本中非最大值加标记
历史版本最大值加标记,历史版本中非最大值加标记
pushdown只是把本层的统计信息进行更新,并且把懒标记传送给左右儿子节点
(2)pushdown怎么传递?
对于pushdown,我们先判断最大值在哪边,如果在左边,那么左边的最大值受到的影响是当前区间对最大值的影响,
而右边的最大值受到的影响仅是当前区间对非最大值的影响(当然可能左右两边都有最大值,那么两边的最大值受到的
影响均是当前区间对最大值的影响)。如果最大值在右边,反过来就可以了。
*/
void pushdown(int u) {
int mx = max(tr[ls].ma, tr[rs].ma);
if (tr[ls].ma == mx)
update(ls, tr[u].add1, tr[u].add3, tr[u].add2, tr[u].add4);
else
update(ls, tr[u].add2, tr[u].add4, tr[u].add2, tr[u].add4);
if (tr[rs].ma == mx)
update(rs, tr[u].add1, tr[u].add3, tr[u].add2, tr[u].add4);
else
update(rs, tr[u].add2, tr[u].add4, tr[u].add2, tr[u].add4);
//更新后懒标记清零
tr[u].add1 = tr[u].add3 = tr[u].add2 = tr[u].add4 = 0;
}
void change_add(int u, int l, int r, int x) {
if (tr[u].l >= l && tr[u].r <= r) {
update(u, x, x, x, x); //区间内每个位置都要加上x
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) change_add(ls, l, r, x);
if (r > mid) change_add(rs, l, r, x);
pushup(u);
}
void change_min(int u, int l, int r, int x) {
if (tr[u].ma <= x) return; //最大值都比x小,不需要进入
if (tr[u].l >= l && tr[u].r <= r && tr[u].se < x) { // 最大值>x 并且 次大值<x,修改最大值
update(u, x - tr[u].ma, x - tr[u].ma, 0, 0);
return;
}
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
if (l <= mid) change_min(ls, l, r, x);
if (r > mid) change_min(rs, l, r, x);
pushup(u);
}
LL querysum(int u, int l, int r) { //查询区间和
if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
LL res = 0;
if (l <= mid) res = querysum(ls, l, r);
if (r > mid) res += querysum(rs, l, r);
return res;
}
int query_ma(int u, int l, int r) { //查询max_a[i]
if (tr[u].l >= l && tr[u].r <= r) return tr[u].ma;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
int res = -INF;
if (l <= mid) res = max(res, query_ma(ls, l, r));
if (r > mid) res = max(res, query_ma(rs, l, r));
return res;
}
//复读机
int query_mb(int u, int l, int r) { //查询max_b[i]
if (tr[u].l >= l && tr[u].r <= r) return tr[u].mb;
pushdown(u);
int mid = (tr[u].l + tr[u].r) >> 1;
int res = -INF;
if (l <= mid) res = max(res, query_mb(ls, l, r));
if (r > mid) res = max(res, query_mb(rs, l, r));
return res;
}
int main() {
//文件输入输出
#ifndef ONLINE_JUDGE
freopen("P6242.in", "r", stdin);
#endif
n = read(), m = read();
build(1, 1, n);
while (m--) {
int op = read(), l = read(), r = read();
if (op == 1) {
int x = read();
change_add(1, l, r, x); //[l,r] -> +x
}
if (op == 2) {
int x = read();
change_min(1, l, r, x); // a[i]=min(a[i],x)
}
if (op == 3) printf("%lld\n", querysum(1, l, r)); //区间和
if (op == 4) printf("%d\n", query_ma(1, l, r)); //当前数组区间最大值
if (op == 5) printf("%d\n", query_mb(1, l, r)); //历史数组区间最大值
}
return 0;
}

View File

@@ -0,0 +1,146 @@
#include <iostream>
#include <cstring>
#define ll long long
using namespace std;
const int N = 2000005;
#define ls u << 1
#define rs u << 1 | 1
// 快读
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
int n, m, op, l, r, k, v;
struct Node {
ll sum;
int l, r, maxa, cnt, se, maxb;
int add1, add2, add3, add4;
} tr[N];
inline void pushup(int u) {
tr[u].sum = tr[ls].sum + tr[rs].sum;
tr[u].maxa = max(tr[ls].maxa, tr[rs].maxa);
tr[u].maxb = max(tr[ls].maxb, tr[rs].maxb);
if (tr[ls].maxa == tr[rs].maxa) {
tr[u].se = max(tr[ls].se, tr[rs].se);
tr[u].cnt = tr[ls].cnt + tr[rs].cnt;
} else if (tr[ls].maxa > tr[rs].maxa) {
tr[u].se = max(tr[ls].se, tr[rs].maxa);
tr[u].cnt = tr[ls].cnt;
} else {
tr[u].se = max(tr[ls].maxa, tr[rs].se);
tr[u].cnt = tr[rs].cnt;
}
}
void build(int l, int r, int u) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].sum = tr[u].maxa = tr[u].maxb = read();
tr[u].cnt = 1, tr[u].se = -2e9;
return;
}
int mid = (l + r) / 2;
build(l, mid, u * 2), build(mid + 1, r, u * 2 + 1);
pushup(u);
}
inline void change(int k1, int k2, int k3, int k4, int u) {
tr[u].sum += 1ll * k1 * tr[u].cnt + 1ll * k2 * (tr[u].r - tr[u].l + 1 - tr[u].cnt);
tr[u].maxb = max(tr[u].maxb, tr[u].maxa + k3);
tr[u].maxa += k1;
if (tr[u].se != -2e9) tr[u].se += k2;
tr[u].add3 = max(tr[u].add3, tr[u].add1 + k3);
tr[u].add4 = max(tr[u].add4, tr[u].add2 + k4);
tr[u].add1 += k1, tr[u].add2 += k2;
}
inline void pushdown(int u) {
int maxn = max(tr[u * 2].maxa, tr[u * 2 + 1].maxa);
if (tr[u * 2].maxa == maxn)
change(tr[u].add1, tr[u].add2, tr[u].add3, tr[u].add4, u * 2);
else
change(tr[u].add2, tr[u].add2, tr[u].add4, tr[u].add4, u * 2);
if (tr[u * 2 + 1].maxa == maxn)
change(tr[u].add1, tr[u].add2, tr[u].add3, tr[u].add4, u * 2 + 1);
else
change(tr[u].add2, tr[u].add2, tr[u].add4, tr[u].add4, u * 2 + 1);
tr[u].add1 = tr[u].add2 = tr[u].add3 = tr[u].add4 = 0;
}
void update_add(int u) {
if (l > tr[u].r || r < tr[u].l) return;
if (l <= tr[u].l && tr[u].r <= r) {
tr[u].sum += 1ll * k * tr[u].cnt + 1ll * k * (tr[u].r - tr[u].l + 1 - tr[u].cnt);
tr[u].maxa += k;
tr[u].maxb = max(tr[u].maxb, tr[u].maxa);
if (tr[u].se != -2e9) tr[u].se += k;
tr[u].add1 += k, tr[u].add2 += k;
tr[u].add3 = max(tr[u].add3, tr[u].add1);
tr[u].add4 = max(tr[u].add4, tr[u].add2);
return;
}
pushdown(u);
update_add(ls), update_add(rs);
pushup(u);
}
void update_min(int p) {
if (l > tr[p].r || r < tr[p].l || v >= tr[p].maxa) return;
if (l <= tr[p].l && tr[p].r <= r && tr[p].se < v) {
int k = tr[p].maxa - v;
tr[p].sum -= 1ll * tr[p].cnt * k;
tr[p].maxa = v, tr[p].add1 -= k;
return;
}
pushdown(p);
update_min(p * 2), update_min(p * 2 + 1);
pushup(p);
}
ll query_sum(int p) {
if (l > tr[p].r || r < tr[p].l) return 0;
if (l <= tr[p].l && tr[p].r <= r) return tr[p].sum;
pushdown(p);
return query_sum(p * 2) + query_sum(p * 2 + 1);
}
int query_maxa(int p) {
if (l > tr[p].r || r < tr[p].l) return -2e9;
if (l <= tr[p].l && tr[p].r <= r) return tr[p].maxa;
pushdown(p);
return max(query_maxa(p * 2), query_maxa(p * 2 + 1));
}
int query_maxb(int p) {
if (l > tr[p].r || r < tr[p].l) return -2e9;
if (l <= tr[p].l && tr[p].r <= r) return tr[p].maxb;
pushdown(p);
return max(query_maxb(p * 2), query_maxb(p * 2 + 1));
}
int main() {
n = read(), m = read();
build(1, n, 1);
while (m--) {
op = read(), l = read(), r = read();
if (op == 1) k = read(), update_add(1);
if (op == 2) v = read(), update_min(1);
if (op == 3) printf("%lld\n", query_sum(1));
if (op == 4) printf("%d\n", query_maxa(1));
if (op == 5) printf("%d\n", query_maxb(1));
}
return 0;
}

View File

@@ -0,0 +1,15 @@
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
for (int i = 0;; i++)
for (int j = 0; j <= i; j++) {
if (i - j == n && i == 4 * j - 30) {
printf("%d %d\n", i, j);
exit(0);
}
}
return 0;
}

View File

@@ -0,0 +1,128 @@
## [区间最值操作与区间历史最值详解](https://www.luogu.com.cn/blog/Hakurei-Reimu/seg-beats)
本文讲解了一种叫做 $Segment$ $Tree$ $Beats$ 的使用线段树维护区间最值操作的方法,和维护区间历史最值的常用手段。主要参考了了吉老师 $2016$ 年集训队论文(见文末参考资料)。
前置知识:**懒标记线段树**。
### 区间最值操作
首先假设我们有一个序列 $A$。区间最值操作是指,给定 $l,r,k$,对于所有 $i\in[l,r]$ ,将 $A_i$变成 $\min(A_i,k)$ 或 $\max(A_i,k)$ 的一种操作。下面由一道例题引入。
[$HDU5306$. $Gorgeous$ $Sequence$](http://acm.hdu.edu.cn/showproblem.php?pid=5306)
> 给出一个长度为 $n(n\leq 10^6)$ 的序列 $A$ 和 $m(m\leq 10^6)$ 次操作,每次操作为以下三种类型之一:
> $1$. 给出 $l,r,k$,对于所有 $i\in[l,r]$,将 $A_i$ 变成 $\min(A_i,k)$
> $2$. 给出 $l,r$,求序列 $A$ 区间 $[l,r]$的 **最大值**
> $3$. 给出 $l,r$,求 $\displaystyle \sum_{i=l}^{r}$​
在这道题中,第一种操作就是 **区间最值操作**。我们发现,关于区间 **最大值的询问** 是可以使用 **带懒标记的线段树维护** 的,而 **区间和则不是很容易求出**,因为这 **不能在下传标记时快速更新答案**。
考虑到区间取 $\min$ 的操作只会对 **最大值超过 $k$ 的节点产生影响**,我们可以在这方面产生思路。为了使复杂度变低,线段树的一个节点要维护三个信息:
- 区间最大值 $mx$
- 区间严格次大值 $se$
- 最大值的个数 $cnt$
那么,一次区间 **最值操作$min$** 作用在这个节点上时,可以被分为以下三种情况:
- $k≥mx$,此时该操作不会对当前节点产生影响,直接退出
- $se<k<mx$,此时这个节点维护的区间中所有最大值都会被修改为 $k$,而最大值个数不变。将区间和加上 $cnt\cdot(k-mx)$ ,打上懒标记,然后退出即可
- $k\leq se$,此时无法快速更新区间信息,因此我们 **需要继续递归到左右子树中**,回溯时合并信息
举个例子,现在要以 $k=2$ 对下面这棵线段树维护的区间取 $\min$。每个节点左侧表示区间最大值,右侧表示严格次大值。
<center><img src='https://cdn.luogu.com.cn/upload/image_hosting/2b77ype7.png'></center>
按照上面描述的操作,我们应该沿着红色的边 $dfs$,最后 **在红色的节点上更新区间和并打上标记**。
这样做的复杂度可以证明是 $O(mlogn)$ 的。
### 支持区间加减
我们在支持上一道例题中所有操作的同时,加入区间加操作。
此时有两种考虑这个问题的方法:
- 套用上一题的做法,我们只需把标记换成二元组 $(add,v)$,表示将当前节点维护的区间先加上 $add$ 再与 $v$ 取最小值。**合并标记** 时,先考虑给当前节点加上一个标记,我们除了更新当前节点的区间加标记 $add$,还需要把它加到 $v$ 上;而给当前节点取最小值可以直接将标记对它取 $\min$。形式地说,当节点 $x$ 下传标记到它的儿子 $ch$ 时,我们把儿子的标记更新为 $(add_{ch}+add_x,\min(v_{ch}+add_x,v_x))$即可。
- 换一种新的思路,我们发现这种方法的本质是把线段树上每个节点维护的元素分成了 **最大值** 和 **非最大值** 这两类,并对它们分别维护。区间最大值标记只会打在 $se<v<mx$ 的节点上,可以看成只针对最大值的加减操作;而区间加操作对于这两类值都生效。分别维护 **区间最大值的加减标记** 和 **非最大值的加减标记**,下传时判断当前节点是否为区间最大值即可。
其中后者看似麻烦一些,但思想很重要,可以扩展到更复杂的问题中(尤其是后面会提到的历史最值问题),因此最好也要理解。如果还不是很明白也没有关系,在下一道例题中会详细讲解。
之前的复杂度分析在这道题无法被沿用。论文中给出了一个基于对标记的分析的 $O(mlog^2n)$ 下界的证明,这个程序的实际运行效率是不错的。
### 同时支持区间最小值和最大值
[$BZOJ4695$. 最假女选手](https://darkbzoj.tk/problem/4695)
> 给出一个长度为 $n(n\leq 5\times 10^5)$ 的序列 $A$ 和 $m(m\leq 5\times 10^5)$ 次操作,每次操作为以下六种类型之一:
> 1. 给出 $l,r,k$,对于所有 $i\in[l,r]$,将 $A_i$ 加上 $k$
> 2. 给出 $l,r,k$,对于所有 $i\in[l,r]$,将 $A_i$ 变成 $\max(A_i,k)$
> 3. 给出 $l,r,k$,对于所有 $i\in[l,r]$,将 $A_i$ 变成 $\min(A_i,k)$
> 4. 给出 $l,r$, 求 $\sum_{i=l}^{r}$
> 5. 给出 $l,r$,求序列 $A$ 区间 $[l,r]$ 的最大值
> 6. 给出 $l,r$,求序列 $A$ 区间 $[l,r]$ 的最小值
在这道题中,区间取 **最大值** 和 **最小值** 的操作同时出现了。虽然我们同样可以套用**第一道例题**的方法,维护区间加、区间取 $\max$,区间取 $\min$ 三个标记,但实现起来会很复杂(可以参考 [$OI-wiki$ 上的题解](https://oi-wiki.org/ds/seg-beats/#bzoj4695))。这里我们可以仍考虑通过划分数域 **将区间最值操作转化为区间加减**。
应这道题的需要,我们将一个区间的元素划分为 **最大值**、**最小值**和 **其他值**三种。首先在每个节点上肯定要维护区间和 $sum$、区间最大值 $mx$ 和最小值 $mi$ 的信息,同时我们也要维护次大值 $semx$、次小值 $semi$ 和最大值最小值的个数 $cmx$,$cmi$。我们分别讨论题目中的三种修改操作。
- 对于加减操作,在线段树上定位区间后直接对三类值同时加上 $k$
- 对于取最小值操作,在线段树上暴力搜索找到 $semx<k<mx$ 的节点。这些节点对应的区间的最大值都应该改成 $k$,只对最大值加上 $k-mx$ 即可
- 取最大值操作同理
那么,我们需要分别维护这三个数域上的加减标记。但是有一些细节要注意:
- 以最大值上的加减标记为例。下传这个标记时要 **判断子区间内是否包含最大值**,如果 **不包含则应下传其他值的加减标记**
- 如果一个区间的值域很小(只有 $1$ 或 $2$ 个数),**可能会发生一个值既是最大值又是次小值这种情况**,也就是发生了数域的重叠。这种情况要特判,分辨到底该被哪个标记作用。
以上两点都会在下面的代码段中体现:
```cpp {.line-numbers}
// add1, add2, add3 分别表示最小值、最大值和其他值的加减标记。
// k1, k2, k3 也是按这个顺序。
void update(int o, int k1, int k2, int k3)
// 作用标记并合并标记,一定要注意顺序
{
if (tr[o].mn1==tr[o].mx1)// 只有一种值时,最大值等于最小值
{
if (k1==k3) k1=k2; else k2=k1; // 不应被其他值的标记作用
tr[o].sum+=1ll*k1*tr[o].cmn;
}
else tr[o].sum+=1ll*k1*tr[o].cmn+1ll*k2*tr[o].cmx
+1ll*k3*(tr[o].r-tr[o].l+1-tr[o].cmn-tr[o].cmx);
if (tr[o].mn2==tr[o].mx1) tr[o].mn2+=k2;
// 次小值等于最大值,应该被最大值标记作用
else if (tr[o].mn2!=INF) tr[o].mn2+=k3;
// 否则应该被其他值标记作用
if (tr[o].mx2==tr[o].mn1) tr[o].mx2+=k1;
else if (tr[o].mx2!=-INF) tr[o].mx2+=k3;
// 对次大值同理
tr[o].mn1+=k1, tr[o].mx1+=k2;
tr[o].add1+=k1, tr[o].add2+=k2, tr[o].add3+=k3;
}
void pushdown(int o) {// 下传标记
int mn=min(tr[lc].mn1, tr[rc].mn1);
int mx=max(tr[lc].mx1, tr[rc].mx1);
// 找出最大值和最小值
update(lc, tr[lc].mn1==mn?tr[o].add1:tr[o].add3,
tr[lc].mx1==mx?tr[o].add2:tr[o].add3, tr[o].add3);
// 下传标记到左子树,若左子树中有最大值则下传最大值加减标记,否则下传其他值加减标记。对最小值同理
update(rc, tr[rc].mn1==mn?tr[o].add1:tr[o].add3,
tr[rc].mx1==mx?tr[o].add2:tr[o].add3, tr[o].add3);
// 右子树同理
tr[o].add1=tr[o].add2=tr[o].add3=0;
}
```
https://www.luogu.com.cn/paste/092htzca
[P4198 楼房重建](https://www.luogu.com.cn/problem/P4198)
[UOJ#515. 【UR #19】前进四](https://uoj.ac/problem/515)
[BZOJ3064. Tyvj 1518 CPU监控](https://darkbzoj.tk/problem/3064)
[SP1557. GSS2 - Can you answer these queries II](https://www.spoj.com/problems/GSS2/)
[UOJ#164. 【清华集训2015】V](http://uoj.ac/problem/164)
[UOJ#169. 【UR #11】元旦老人与数列](http://uoj.ac/problem/169)
[P6242 【模板】线段树 3](https://www.luogu.com.cn/problem/P6242)