算法学习笔记(14):区间最值操作和历史最值问题
区间最值操作, 历史最值问题
来源
吉老师2016集训队论文, oiwiki, 网络上各种博客。
概述
区间最值操作指的是:
将所有的$i \in $ \((l, r)\), \(a_i = min或max(a_i, k)\)。
历史最值问题指的是:
新定义一个数组 \(b[]\), \(b[i] = max或min(b[i], a[i])\)。还有一种是历史版本和, 即\(b[i] = \sum a[i]\)。
再对 \(b[i]\) 进行一系列询问。
询问 \(\sum_l^r b_i\), 或者 \(max_l^r b_i\), \(min_l^r b_i\)
区间最值操作
就考虑一个简单的问题。
1 l r
表示询问 \((l, r)\) 的区间和。
1 l r k
表示将所有 \(i \in (l, r)\)的 \(a_i\) 变成 $min(a_i, k) $。
考虑小于等于 \(k\) 的数不会受影响, 那么做法即为:
对于线段树的一个节点, 维护 \(sum\) 表示区间和, \(mx\) 表示区间最大值, \(se\) 表示区间严格次大值, \(cnt\) 表示区间最大值的个数。
-
若 \(mx <= k\),不影响答案, return。
-
若 \(se <= k < mx\), 会影响最大值, 所以用上述信息更改答案, 当节点 \(p\) 被操作时,只修改其最大值。
-
若 \(k < se\), 会影响更多的值, 要递归下去找到所有的满足条件2的节点, 更改完以后回溯上来。 如果修改了严格次大值和最大值,则将这两个合并,然后递归两个儿子,看是否需要修改, 合并后对当前节点需要从儿子合并上来新的严格次大值和最大值。
1.如果有一个儿子内含有最大值和严格次大值,则递归这个儿子进行合并。
2.否则我们修改两个儿子的最大值,这两个儿子的严格次大值不需要改动,并用来更新父亲的严格次大值
修改完儿子后,合并上来新的严格次大值和最大值
递归完以后所有节点的严格次大值都是正确的, 考虑操作二时只更新了一层儿子最大值, 所以我们需要 pushdown 一下, 实际上我不需要多定义一个标记, 因为标记始终与已经修改后的最大值相等, 所以只需要判断左右儿子哪个是最大值即可, 然后将较大的 \(mx\), 赋值为父亲的 \(mx\)。 考虑如果递归到较小的儿子, 那么用如上的判断可能是错误的。
也就是如下代码
void pd(int p) {
int w = max(t[ls].mx, t[rs].mx);
if(t[ls].mx == w) t[ls].mx = t[p].mx;
if(t[rs].mx == w) t[rs].mx = t[p].mx;
}
}
实际上这是正确代码, 不同于 OI-WIKI, 我们不需要打 tag, 考虑当儿子的 \(mx\) 为我们刚开始打标记时的最大值时, 我们才修改它的 \(mx\), 我们称其为真最大值, 我们考虑从刚开始打标记时的节点开始 pushdown, 左右儿子一定有一个是真最大值, 我们修改它, 另一个可能是, 可能不是, 但一定会到某一层, 它不是。 但是我们的代码为 t[ls].mx = t[p].mx
, 所以是正确的。
这很好理解吧。。。实在不懂手动模拟一下。
考虑, 每次递归会把线段树节点的最大值和严格次大值合并, 而线段树节点大小和为 \(O(nlogn)\), 所以复杂度为 \(O(nlogn)\)。
历史最值问题
考虑一个简单的问题。
1 l r k
表示将所有 \(i \in (l, r)\) 的 \(a_i\) 加上 \(k\)。 \(k\) 可以为负。
2 l r
表示询问所有$ i \in (l, r)$ 的 \(b_i\) 的最大值。
考虑对于线段树每个节点维护一个 \(mx\) 表示区间最大值, \(mx'\) 表示区间最大历史最大值。\(add\) 表示区间加标记, \(add'\) 表示区间最大历史最大值加标记。 考虑给一个区间区间加的时候, 会在最大值上面加一个 \(k\), 区间最大历史最大值可能会加一个 \(k\)。考虑在还没有下传标记前, 我们会一直给一些区间打标记, \(add\) 会变化很多次, 那么 \(add'\) 就是存储其中最大的 \(add\)。正确性显然。
那么考虑一下pushdown。将 \(add_p\) 和 \(add_p'\) 下传。
\(add_{ch} = add_{ch} + add_p\)
\(add_{ch}' = max(add_{ch}', add_{ch} + add_p')\)
其他就是基本操作了。
P6242 【模板】线段树 3(区间最值操作、区间历史最值)
考虑就是把区间最值操作和历史最值问题结合起来, 区间最值操作就是前面的直接搬过来, 但是历史最值的代码要改一下。 因为有了区间最值操作, 就会有一个对最大值进行加减的操作, 这与区间加不同。 所以要把最大值的加标记和非最大值加标记分开来。 也就是维护 \(add1\), \(add1'\),\(add2\), \(add2'\) 分别表示最大值的加标记, 区间最大历史最大值的加标记, 非最大值的加标记, 非区间最大值的历史最大值加标记。
考虑怎么理解这四个标记, 前面的历史最值问题的一个标记是因为区间加覆盖了整个节点的区间。 而现在有可能只对最大值进行区间加, 所以要把原来的 \(add\) 拆成 \(add1\) 和 \(add2\)。
但是又考虑 \(add2'\) 存在的意义是什么, 因为 \(add2\) 是非最大值的加标记, 我们只询问历史最大值的区间最大值, \(add2'\) 似乎就没有什么意义, 只要有 \(add1'\) 就够了。 但是考虑pushdown的时候, 有两个儿子, 如果某一个儿子最大值不等于这个点的最大值, 那么儿子区间最大历史最大值就要用 \(add2\) 和 \(add2’\) 去更新。 这就是 \(add2'\) 存在的原因。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e5 + 10;
const int INF = 1e18;
int n, m, a[N];
struct JI_ST{
struct Node{
int l, r, sum, mx, _mx, se, cnt, add1, _add1, add2, _add2;
}t[N << 2];
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid (t[p].l + t[p].r >> 1)
void update(int p, int d1, int _d1, int d2, int _d2) {
t[p].sum += d1 * t[p].cnt + d2 * (t[p].r - t[p].l + 1 - t[p].cnt);
t[p]._mx = max(t[p]._mx, t[p].mx + _d1);
t[p]._add1 = max(t[p]._add1, t[p].add1 + _d1);
t[p].add1 += d1;
t[p]._add2 = max(t[p]._add2, t[p].add2 + _d2);
t[p].add2 += d2;
t[p].mx += d1;
if (t[p].se != -INF) t[p].se += d2;
}
void pushdown(int p) {
int MAX = max(t[ls].mx, t[rs].mx);
if (t[ls].mx == MAX) update(ls, t[p].add1, t[p]._add1, t[p].add2, t[p]._add2);
else update(ls, t[p].add2, t[p]._add2, t[p].add2, t[p]._add2);
if (t[rs].mx == MAX) update(rs, t[p].add1, t[p]._add1, t[p].add2, t[p]._add2);
else update(rs, t[p].add2, t[p]._add2, t[p].add2, t[p]._add2);
t[p].add1 = t[p]._add1 = t[p].add2 = t[p]._add2 = 0;
}
void pushup(int p) {
t[p].sum = t[ls].sum + t[rs].sum;
t[p]._mx = max(t[ls]._mx, t[rs]._mx);
if (t[ls].mx == t[rs].mx) {
t[p].mx = t[ls].mx;
t[p].se = max(t[ls].se, t[rs].se);
t[p].cnt = t[ls].cnt + t[rs].cnt;
}
else if (t[ls].mx > t[rs].mx) {
t[p].mx = t[ls].mx;
t[p].se = max(t[ls].se, t[rs].mx);
t[p].cnt = t[ls].cnt;
}
else {
t[p].mx = t[rs].mx;
t[p].se = max(t[rs].se, t[ls].mx);
t[p].cnt = t[rs].cnt;
}
}
void build(int p, int l, int r) {
t[p].l = l, t[p].r = r;
if (l == r) return t[p] = {l, r, a[l], a[l], a[l], -INF, 1, 0, 0, 0, 0}, void();
build(ls, l, mid); build(rs, mid + 1, r);
pushup(p);
}
void add_modify(int p, int x, int y, int z) {
if (x <= t[p].l && t[p].r <= y) return update(p, z, z, z, z);
pushdown(p);
if (x <= mid) add_modify(ls, x, y, z);
if (y > mid) add_modify(rs, x, y, z);
pushup(p);
}
void min_modify(int p, int x, int y, int z) {
if (t[p].mx <= z) return;
if (x <= t[p].l && t[p].r <= y && z >= t[p].se) return update(p, z - t[p].mx, z - t[p].mx, 0, 0);
pushdown(p);
if (x <= mid) min_modify(ls, x, y, z);
if (y > mid) min_modify(rs, x, y, z);
pushup(p);
}
int Sum(int p, int x, int y) {
if (x <= t[p].l && t[p].r <= y) return t[p].sum;
pushdown(p); int res = 0;
if (x <= mid) res += Sum(ls, x, y);
if (y > mid) res += Sum(rs, x, y);
return res;
}
int Max(int p, int x, int y, int op) {
if (x <= t[p].l && t[p].r <= y) return (op == 0) ? t[p].mx : t[p]._mx;
pushdown(p); int res = -INF;
if (x <= mid) res = max(res, Max(ls, x, y, op));
if (y > mid) res = max(res, Max(rs, x, y, op));
return res;
}
}T;
signed main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
}
T.build(1, 1, n);
for (int i = 1, op, l, r, x; i <= m; i++) {
scanf("%lld%lld%lld", &op, &l, &r);
switch(op) {
case 1: scanf("%lld", &x); T.add_modify(1, l, r, x); break;
case 2: scanf("%lld", &x); T.min_modify(1, l, r, x); break;
case 3: printf("%lld\n", T.Sum(1, l, r)); break;
case 4: printf("%lld\n", T.Max(1, l, r, 0)); break;
case 5: printf("%lld\n", T.Max(1, l, r, 1)); break;
}
}
return 0;
}
P4314 CPU 监控
带区间覆盖的历史最大值。 我们考虑从上一次下传标记到现在, 我们在这个点累积了一个操作队列, 考虑这样一个问题三种情况:
- 考虑只有加法标记。
- 考虑只有覆盖标记。
- 加法标记中掺杂了覆盖标记。
1, 2 都十分简单, 多维护历史最大加标记和历史最大覆盖标记即可。 考虑第 3 种情况, 我们可以把覆盖之后的加法标记都看作覆盖标记, 所以照样按照1, 2的方法维护即可, 只是要先下传加法标记在下穿覆盖标记。 加法标记下传只是为了更新历史最大值。
点击查看代码
#include<bits/stdc++.h>
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid (t[p].l + t[p].r >> 1)
using namespace std;
const int N = 1e5 + 10;
const int INF = 1e9;
int n, q, a[N];
struct Node{
int l, r, mx, hmx, ad, had, cv, hcv;
bool vis;
}t[N << 2];
void pu(int p) {
t[p].mx = max(t[ls].mx, t[rs].mx);
t[p].hmx = max(t[ls].hmx, t[rs].hmx);
}
void upd_ad(int p, int _ad, int _had) {
if(t[p].vis) {
t[p].hcv = max(t[p].hcv, t[p].cv + _had);
t[p].cv += _ad;
t[p].hmx = max(t[p].hmx, t[p].mx + _had);
t[p].mx += _ad;
} else {
t[p].had = max(t[p].had, t[p].ad + _had);
t[p].ad += _ad;
t[p].hmx = max(t[p].hmx, t[p].mx + _had);
t[p].mx += _ad;
}
}
void upd_cv(int p, int _cv, int _hcv) {
if(t[p].vis)
t[p].hcv = max(t[p].hcv, _hcv);
else
t[p].vis = 1, t[p].hcv = _hcv;
t[p].mx = t[p].cv = _cv;
t[p].hmx = max(t[p].hmx, _hcv);
}
void pd(int p) {
//不能写 if(t[p].ad) 因为可能t[p].ad为0了, 但是t[p].had有值
upd_ad(ls, t[p].ad, t[p].had);
upd_ad(rs, t[p].ad, t[p].had);
t[p].ad = t[p].had = 0;
//不能写 if(t[p].vis) 可能是赋值为 0
if(t[p].vis) {
upd_cv(ls, t[p].cv, t[p].hcv);
upd_cv(rs, t[p].cv, t[p].hcv);
t[p].cv = t[p].hcv = t[p].vis = 0;
// vis 也是从上次下传到现在有没有被cv过, 所以要清零。
}
}
void mdf(int p ,int x, int y, int z, int op) {
if(y < t[p].l || t[p].r < x) return;
if(x <= t[p].l && t[p].r <= y) return op == 1 ? upd_ad(p, z, z) : upd_cv(p, z, z);
pd(p); mdf(ls, x, y, z, op); mdf(rs, x, y, z, op); pu(p);
}
int qry_mx(int p, int x, int y) {
if(y < t[p].l || t[p].r < x) return -INF;
if(x <= t[p].l && t[p].r <= y) return t[p].mx;
return pd(p), max(qry_mx(ls, x, y), qry_mx(rs, x, y));
}
int qry_hmx(int p, int x, int y) {
// printf("%d %d\n", t[p].l, t[p].r);
if(y < t[p].l || t[p].r < x) return -INF;
if(x <= t[p].l && t[p].r <= y) return t[p].hmx;
return pd(p), max(qry_hmx(ls, x, y), qry_hmx(rs, x, y));
}
void bld(int p, int l, int r) {
t[p].l = l, t[p].r = r;
if(l == r) return t[p] = {l, r, a[l], a[l], 0, 0, 0, 0, 0}, void();
bld(ls, l, mid); bld(rs, mid + 1, r); pu(p);
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]);
bld(1, 1, n);
char op[5];
scanf("%d", &q);
for(int i = 1, l, r, x; i <= q; i++) {
scanf("%s%d%d", op, &l, &r);
// puts("fuck");
switch(*op) {
case 'Q': printf("%d\n", qry_mx(1, l, r)); break;
case 'A': printf("%d\n", qry_hmx(1, l, r)); break;
case 'P': scanf("%d", &x); mdf(1, l, r, x, 1); break;
case 'C': scanf("%d", &x); mdf(1, l, r, x, 0); break;
}
}
return 0;
}