线段树

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\)。有以下方式:

  1. 暴力维护标记,即 \(k^2\) 个可能的项的系数的变化量,相对复杂。
  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;
}

历史版本和

难点:标记合并。

我们考虑许多连续打上的同种标记对版本和的贡献能否合并。常见:

  1. 区间加 / 乘:对历史最大值的贡献即为标记前缀和的最大值与 \(0\)\(\max\),对和的贡献显然。
  2. 区间推平:对最大值贡献为标记最大值,对和的贡献显然。

注意对和的贡献都要乘上各个标记存在的 时间

然后考虑不同种标记的合并,可以发现推平之后的加 / 乘标记可以看做推平。

细节

为了便于理解,我们可以把标记看成一个队列的形式。我们要在支持下传的基础上,用尽可能少的信息概括这个队列对区间值的影响。

就历史和而言,我们可以发现假如支持区间加、区间推平,其对一个节点历史和所有的贡献应该形如 \(a\cdot sum + b\cdot len\),其中 \(sum, len\) 分别为区间的 原始和(即儿子被下传标记前的区间和)以及长度。当进行第一次推平之后,以后的加操作可以看作推平操作,也只会改变 \(b\) 而非 \(a\),此时整个区间的原始和不影响贡献。

另一个 trick 是,考虑在每次全局 \(a\) 自增 \(1\)(注意对于一些区间,可能会转化为 \(b\) 自增 \(x\)\(x\) 为这个区间目前被推平为的值),这样自动维护了区间历史和。

Trick

考虑序列上静态函数 \(v\),则对

\[\sum_{i=l}^r\sum_{j=x}^y v(i,j) \]

的查询可以转化为 扫描线 + 历史版本和

首先差分,令 \(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 解决。

posted @ 2023-05-12 08:24  音街ウナ  阅读(63)  评论(0)    收藏  举报