线段树
Segment Beats
支持区间取 \(\max\) 的结构。复杂度证明依赖于值域的势能变化。对于区间加,复杂度依赖于区间值域的整体变化,非紧上界为小常数 \(O((q+n)\log^2n)\)。
即,区间维护最大值、次大值,只有所询问的数小于次大值才有必要继续递归。
另外:单纯维护区间历史 \(\max\) 并不需要借助这种方法,可以采用历史版本和。
区间多项式维护
考虑现在有一种关于 \(a_i,b_i\) 的 \(k\) 次多项式,我们想在 \(O(k^2\log n)\) 的复杂度内支持区间修改 \(a, b\),查询上述多项式取 \(i\in[l,r]\) 的值之和。最简单的一类问题,即维护 \(a_i\cdot b_i\)。有以下方式:
- 暴力维护标记,即 \(k^2\) 个可能的项的系数的变化量,相对复杂。
- 矩阵维护。
一般可以支持区间推平、区间加、区间乘的部分修改。而且利用上多项式的思想,甚至我们也可以把 \(s_i\) 看做第三元,直接维护历史版本和。
前缀最值
我们设一种结合律运算 \(\otimes\),要求查询:一个区间内所有前缀严格 / 非严格最大值的位置的权值,在这种运算意义下的和。以非严格为例。
关键在于 pushup 时,对左儿子的最大值 \(x\) 小于右儿子最大值的情况下进行维护。
设 \(c([l,r],x)\) 表示 \([l,r]\) 区间中 \(≥x\) 的部分的前缀严格最大值的 \(\otimes\) 和,显然合并 \([l,mid],(mid,r]\) 产生的前缀严格最大值的和为 \(s[lc]\otimes c((mid,r],mx[lc])\)。
考虑暴力递归,查询 \(c\)。由于 \(x\) 固定,可以发现根据 \(mx[lc]\) 的取值,可直接决定递归的方向,类似于平衡树中的查询排名。不过由于显然 \(s[p] \neq s[lc] \otimes s[rc]\),因此 向左儿子递归时,加上的右儿子的贡献 不能是 \(s[rc]\) 而应该是 \(c((mid,r],mx[lc])\)。若 \(\otimes\) 满足可减性则可以写为 \(s[p] - s[lc]\)。
若不满足可减性也容易做,只需在每个节点处额外存储计算出的 \(c((mid,r],mx[lc])\) 的值。但模板题中的 \(\otimes\) 是 \(+\)。
于是完成了 pushup 部分。最后,查询得到的节点有 \(O(\log n)\) 个,考虑合并答案的问题。其实同理,相当于从左到右依次合并进来,每次合并两个节点 \(p,q\) 相当于 pushup,查询 \(c(q,mx[p])\) 即可。
所以我们做到 \(O(\log^2n)\) 查询。
Luogu P4198 楼房重建
模板题,\(\otimes\) 为不同值数目,即求前缀严格最大值数目。
DTOJ 5232 莫队
不重复出现问题,考虑用 nxt 套路解决。\(g_i\) 表示 \(\min_{j=i}^r nxt_j\),就是一个后缀最值的形式。带修求单个 \(g_i\) 可以直接线段树二分(注意 \(g\) 的取值与 \(r\) 有关),而此题答案为 \(g\) 的区间和减去左端点之和 \(\dfrac{(r-l+1)(l+r)}{2}\)(等差数列)。
考虑 提取严格 后缀最值。即得 \(\otimes\) 就是此最值乘上管辖区间长度之和。同理可以写出 \(c([l,r],x)\) 函数,若 \(x\ge mi[rc]\) 因为这里满足可减性贡献就是 \(s[p]-s[rc]+c((mid,r],x)\),否则整个右儿子都是 \(x\) 作最小值的管辖区间,贡献为 \(c([l,mid],x)+(r-mid)x\)。
这里必须考虑合并顺序的问题。因为是后缀而非前缀最值,所以不仅 \(pushup\) 时要在左半查询右半,而且 查询的 \(O(\log n)\) 个区间合并也要从右到左。只需要查询时先递归右边再递归左边即可。
#include <bits/stdc++.h>
#define ls(p) (p << 1)
#define rs(p) (p << 1 | 1)
#define mid ((l + r) / 2)
using namespace std;
const int maxn = 400005;
typedef long long ll;
set<int> st[maxn];
int n, m, a[maxn], nxt[maxn];
ll t[maxn << 2];
int mi[maxn << 2];
// c
ll get(int p, int l, int r, int x) {
if (mi[p] > x) return (r - l + 1) * 1ll * x;
else if (l == r) return min(mi[p], x);
else if (x >= mi[rs(p)]) return t[p] - t[rs(p)] + get(rs(p), mid + 1, r, x);
else return get(ls(p), l, mid, x) + (r - mid) * 1ll * x;
}
void pushup(int p, int l, int r) {
t[p] = t[rs(p)] + get(ls(p), l, mid, mi[rs(p)]);
mi[p] = min(mi[ls(p)], mi[rs(p)]);
}
void build(int p, int l, int r) {
if (l == r) mi[p] = t[p] = nxt[l];
else build(ls(p), l, mid), build(rs(p), mid + 1, r), pushup(p, l, r);
}
void modify(int p, int l, int r, int i, int x) {
if (l == r) return mi[p] = t[p] = x, void();
else if (i <= mid) modify(ls(p), l, mid, i, x);
else modify(rs(p), mid + 1, r, i, x);
pushup(p, l, r);
}
ll ans;
int qv;
// CAUTION: 右儿子先递归!!因为合并顺序和 楼房重建 不同,是从右到左,右询问左。
inline void ask(int p, int l, int r, int ql, int qr) {
if (l > qr || r < ql) return;
else if (ql <= l && r <= qr) ans += get(p, l, r, qv), qv = min(qv, mi[p]);
else ask(rs(p), mid + 1, r, ql, qr), ask(ls(p), l, mid, ql, qr);
}
signed main() {
int z, x, y;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), st[i].insert(n + 1);
for (int i = n; i; --i)
nxt[i] = *(st[a[i]].begin()), st[a[i]].insert(i);
build(1, 1, n);
while (m--) {
scanf("%d%d%d", &z, &x, &y);
if (z == 1) {
auto it = st[a[x]].find(x), rt = it;
int rp = *(++rt);
if (it != st[a[x]].begin()) --it, modify(1, 1, n, *it, rp);
st[a[x]].erase(x);
it = st[y].lower_bound(x), a[x] = y;
modify(1, 1, n, x, *it);
if (it != st[y].begin()) --it, modify(1, 1, n, *it, x);
st[y].insert(x);
} else {
ans = 0, qv = y + 1, ask(1, 1, n, x, y);
printf("%lld\n", ans - 1ll * (x + y) * (y - x + 1) / 2);
}
}
return 0;
}
历史版本和
难点:标记合并。
我们考虑许多连续打上的同种标记对版本和的贡献能否合并。常见:
- 区间加 / 乘:对历史最大值的贡献即为标记前缀和的最大值与 \(0\) 取 \(\max\),对和的贡献显然。
- 区间推平:对最大值贡献为标记最大值,对和的贡献显然。
注意对和的贡献都要乘上各个标记存在的 时间。
然后考虑不同种标记的合并,可以发现推平之后的加 / 乘标记可以看做推平。
细节
为了便于理解,我们可以把标记看成一个队列的形式。我们要在支持下传的基础上,用尽可能少的信息概括这个队列对区间值的影响。
就历史和而言,我们可以发现假如支持区间加、区间推平,其对一个节点历史和所有的贡献应该形如 \(a\cdot sum + b\cdot len\),其中 \(sum, len\) 分别为区间的 原始和(即儿子被下传标记前的区间和)以及长度。当进行第一次推平之后,以后的加操作可以看作推平操作,也只会改变 \(b\) 而非 \(a\),此时整个区间的原始和不影响贡献。
另一个 trick 是,考虑在每次全局 \(a\) 自增 \(1\)(注意对于一些区间,可能会转化为 \(b\) 自增 \(x\),\(x\) 为这个区间目前被推平为的值),这样自动维护了区间历史和。
Trick
考虑序列上静态函数 \(v\),则对
的查询可以转化为 扫描线 + 历史版本和。
首先差分,令 \(f(l, r, x) = \sum_{i =l}^r \sum_{j=\textbf{1}}^x g(i, j)\)。询问即求 \(f(l, r, y) - f(l, r, x - 1)\)
按正常方法,从左到右枚举 \(j\),用线段树维护 \(g(i, j)\) 的值,保存在位置 \(i\) 上。
于是发现 \(f(l, r, x)\) 即为扫描线过程中,\([l, r]\) 在前 \(x\) 个版本上的和。
特别地,若扫描线需要进行的操作恰好为单点加、区间历史版本和,我们可以用一个树状数组维护区间和的 trick 解决。

浙公网安备 33010602011771号