《浅谈函数最值的动态维护》阅读随笔
\忆哀/\忆哀/\忆哀/\忆哀/
我不觉得我能在短期内啃下这篇论文
10k预定了
好的我写完了 确实 10k
摘要
咱整点好活!现在我们可以维护一堆函数的最值了,复杂度优于以往做法。
区间加等差数列区间最值问题现在可以做到 \(\mathcal O(\log^2)\) 了!
李超树维护的信息现在可以做到 \(\mathcal O(\alpha(n) \log n)\) 插入 \(\mathcal O(\log n)\) 查询了!
忆哀出品,必属精品~
1 概述
我们希望解决的问题可以陈述如下:
给定一个函数列 \(f_i : \mathbb R \to \mathbb R\)。我们希望支持于函数列上进行一些修改,且至少能查询全局的最值。本文以最大值为例。
为了查询最大值,肯定需要维护所有函数取最大值后得到的分段函数,即这些函数的上包络线。这启发我们首先寻找分段函数段数的界。
首先假设我们需要维护的函数都是次数不超过 \(s\) 的多项式,这也就说明了任意两个函数合并得到的分段函数不会超过 \(s+1\) 段。不妨将分段函数记作一个序列,每个元素代表着一段极大区间对应的函数的编号。由本段首句我们可以知道,这个序列中任意子序列不可能是 \(x,y\text{ s.t. }x \not= y\) 交替 \(s+1\) 次以上形成的序列。
我们现在需要找到这个序列长度的界。
引入如下定义:
\((n,s)\text{ Davenport-Schinzel}\) 序列
记一个长度为 \(m\) 的序列 \(a\) 是一个 \((n,s)\text{ Davenport-Schinzel}\) 序列(简记作 \(DS(n, s)\) 序列),当且仅当 \(\forall a_i\in [1,n]\cap N\),且满足:
- \(a\) 中相邻两项值不同。
- 对于任意 \(x,y\text{ s.t. }x \not= y\),\(x,y\) 交替构成的序列如果是 \(a\) 的子序列,则长度不超过 \(s+1\)。
容易发现我们需要求的就是 \(DS(n,s)\) 序列的渐进界。记它为 \(\lambda_s(n)\),我们有神秘结论:
证明指路:Seth Pettie, Sharp Bounds on Davenport-Schinzel Sequences of Every Order, 2015
所以我们现在能知道任意 \(n\) 个次数不超过 \(s\) 的多项式所对应图像的上包络线分段数不超过 \(\lambda_s(n)\),即上包络线分段数是近乎线性的。
另一个问题关注于在区间上定义的 \(s\) 次多项式。我们不妨将这类多项式在区间外的取值设为 \(-\infty\),容易知道任意两个在区间上定义的 \(s\) 次多项式的交替次数不会超过 \(s + 2\)。因此任意 \(n\) 个次数不超过 \(s\) 的多项式所对应图像的上包络线分段数不超过 \(\lambda_{s+2}(n)\)。
先前 OI 界研究的问题多为一次函数或定义在区间上的一次函数,这两个问题分别有 \(s = 1\) 和 \(s = 3\)。
在接下来的讨论中,主要有两个方向:
- 不对函数进行修改,仅作为一个集合维护。这可以通过二进制分组的思想设计算法。
- 询问最大值的位置保证单调递增。这可以通过类似 Segment Tree Beats! 的思路进行设计。
2 函数集合维护
2.1 综述
给定一个 \(n\) 长度函数列 \(f_i : \mathbb R \to \mathbb R\)。我们需要对 \(x_1,x_2,\dots,x_m\) 中的每个 \(x_k\),求 \(\max_{i=1}^n f_{i}(x_k)\)。
当所有函数初始给定时,我们可以分治维护上包络线。我们每次将函数列分为两部分处理,然后将两部分的包络线合并。容易发现复杂度为
主定理有 \(T(n) = \mathcal O(\lambda_s(n)\log n)\)。得到序列后回答询问的时间为 \(\mathcal O(m\log n)\)。
如果函数是逐一给定的,我们可以采用二进制分组的方式加以解决。
不难发现这样合并的时间复杂度为 \(\mathcal O(\lambda_s(n)\log n)\),空间为 \(\mathcal O(\lambda_s(n))\)。为啥空间是这个?
由于共有 \(\mathcal O(\log n)\) 个组,我们单次查询复杂度是 \(\mathcal O(\log^2n)\) 的。
事实上,我们能够做到 \(\mathcal O(\log n)\) 查询。
对比 \(s=3\) 的情况,本算法支持插入线段,其均摊复杂度是 \(\mathcal O(\alpha(n) \log n)\) 的,而询问可以做到 \(\mathcal O(\log n)\),优于李超线段树。
2.2 朴素实现
记 \(k = \left\lfloor\log_2 n\right\rfloor\)。令 \(n\) 的二进制分解为 \(\sum_{i=0}^{k} n_i \times 2^{i},\ n_i \in \{0,1\}\)。我们将函数部分地合并为函数组,每个组维护该组内函数的上包络线。记大小为 \(2^i\) 的函数组为 \(L_i\)。当加入一个函数时首先让它自成一组,随后若存在两组大小相同的函数组,就将它们合并。合并一直进行直到没有大小相同的两个函数组。容易发现将所有函数加入后存在大小为 \(2^i\) 的函数组当且仅当 \(n_i = 1\)。
整个过程可以看作不完全地执行大小为 \(2^{k+1}\) 的分治,因此总复杂度为 \(\mathcal O(\lambda_s(2^k) k) = \mathcal O(\lambda_s(n)\log n)\)。
查询部分朴素地在每个块上二分找到查询坐标所在的段,时间复杂度为 \(\mathcal O(\log^2 n)\)。
2.3 分散层叠
我们维护辅助函数组 \(T_k = L_k\)。对于 \(0\le j <k\),我们将 \(T_{j+1}\) 中所有 \(3\) 的倍数位置取出,与 \(L_j\) 归并得到 \(T_j\)。在 \(T_j\) 中记录每个点的来源,以及两种来源的前趋后继,我们就能在 \(\mathcal O(1)\) 的时间内通过在 \(T_j\) 中的后继找到在 \(L_j\) 和 \(T_{j+1}\) 中的后继。根据分散层叠,查询的复杂度为 \(\mathcal O(\log n)\)。
然后是一通渐进。我们首先知道任意时刻 \(|L_j |\le \lambda_s(2^j)\),并且有 \(\lambda_s(n) = o(n^{1+\epsilon})\)。然后做渐进:
由于二进制分组,重构特定函数组的复杂度渐进地等于重构所有大小小于等于此特定函数组的复杂度。因此维护总时间复杂度仍为 \(\mathcal O(\lambda_s(n)\log n)\)。
因此做到了均摊 \(\mathcal O(\alpha(n) \log n)\) 插入 \(\mathcal O(\log n)\) 查询。
2.4 应用
2.4.1 维护分段一次函数最值
当该方法用于维护分段一次函数的时候,\(s = 3\)。复杂度有 \(\mathcal O(\lambda_3(n)\log n) = \mathcal O(n\ \alpha(n)\log n)\)。这在理论复杂度上优于李超线段树单次插入 \(\mathcal O(\log^2 n)\) 的复杂度。
需要注意的是,存在使 \(n\) 条线段的上包络线分段数为 \(\mathcal O(n \ \alpha (n))\) 的构造,因此上述复杂度是紧的。
构造详见 Ady Wiernik, Micha Sharir, Planar realizations of nonlinear davenport-schinzel sequences by segments, 1988。
2.4.2 优化动态规划问题
对于形如 \(a_i = \max_{j < i} a_j + w_{j,i}\) 的动态规划问题,若 \(a_j + w_{j,i}\) 能改写成 \(f_j(x_i)\) 的形式,且函数族 \(f\) 的交替阶数较低,我们就可以通过如上的做法进行转移。
在以往的问题中,我们需要将 \(f\) 写成一次函数的形式后采用李超线段树转移,或者依赖 \(w\) 的决策单调性转移。
可以证明的是,这两种情况都是 \(s=1\) 时的特例。对于将 \(f\) 写作一次函数形式的方法显然。对于决策单调性,我们通常要求 \(w\) 满足四边形不等式,从而得到 \(f_{i}(j+1) + f_{i+1}(j) \le f_i(j) + f_{i+1}(j+1)\)。这表出了 \(\Delta f_i \le \Delta f_{i+1}\),也即 \(0\le \Delta (f_{i+1} - f_i)\)。
这就说明对于可以通过四边形不等式优化的问题,对于 \(i\le i_0\) 的函数族 \(f\) 在定义域 \(\{j : i_0 \le j\}\) 上是 \(s=1\) 阶交替的。
上述结构在决策单调性问题上并不能做到最优,但是具有普适性,能够处理性质略复杂的转移函数。
3 询问点单调递增
设有 \(n\) 个函数 \(f_i : \mathbb R \to \mathbb R\)。我们需要对 \(x_1 < x_2<\cdots<x_m\) 中的每个 \(x_k\),求 \(\max_{i=1}^n f_{i}(x_k)\)。
接下来的讨论假设了函数族是 \(s\) 阶交替的,且交替点可以 \(\mathcal O(1)\) 求得。
3.1 \(\text{Kinetic Tournament Tree}\)
我们维护一棵线段树,其每个叶子节点表示一个函数,每个节点存储了当前时间下子树中叶子节点的函数在当前 \(x\) 出取得最值的函数的编号。在 \(x\) 增加的过程中,我们需要不断修改某些节点的取值。我们对每个节点维护使得最大值对应函数第一次发生改变的 \(x\),当当前询问超过 \(x\) 时递归修改子树中发生替换的部分。
我们称维护该信息的线段树为 Kinetic Tournament Tree,简称 KTT。
我们考虑线段树上每个节点被修改的次数,这等价于每个节点中函数上包络线的分段数,因此所有节点修改次数总和与分治相同,为 \(\mathcal O(\lambda_s(n)\log n)\)。由于目前是在线段树上维护,每次需要花费 \(\mathcal O(\log n)\) 的复杂度到达修改节点,因此总复杂度为 \(\mathcal O(\lambda_s(n) \log^2n)\)。它的复杂度略逊于分治,但是其有着更强的应用。
3.2 带修函数列最值问题
现在需要支持在线地将某个函数重赋值。设总共修改 \(m\) 次,询问 \(q\) 次。
考虑 KTT。在修改函数时,我们将对应叶子修改,同时更新到根节点的链。
现在证明总修改复杂度为 \(\mathcal O(\lambda_{s+2}(n + m)\log^2 n)\)。
我们将维护过程等价转化:假设第 \(i\) 个叶子被修改了 \(k_i\) 次,我们转化成在该节点处挂 \(k_i + 1\) 个函数,第一个函数是初始函数,后面是 \(k_i\) 次修改对应的函数。则一个对应线段为 \([l,r]\) 的节点的子树里的上包络线分段数不会超过 \(\lambda_{s+2}(r - l + 1 + \sum k_i)\)。因此同一层的分段数不会超过 \(\sum_{[l,r]} \lambda_{s+2}(r - l + 1 + \sum k_i)\)。渐进地,将求和号放在 \(\lambda_s()\) 中进行可以得到其不大于 \(\lambda_{s+2}(n + m)\)。
因此总分段数不会超过 \(\lambda_{s+2}(n + m)\log n\)。
因此,在线单点修改全局查询的问题中,施 KTT 可以做到 \(\mathcal O(\lambda_{s+2}(n + m)\log^2 n + q)\) 的时间复杂度。
4 线性情况的拓展
由于一次函数的性质良好,我们可以进行一些拓展。
目前没有在更高次函数上的相似证明与应用。
4.1 包含两类区间修改的序列最值问题
给定序列 \(k, b\)。有如下三种操作:
- \((l,r,x)\)。对于 \(l\le i\le r\),令 \(b_i \leftarrow k_i x + b_i\)。
- \((l,r,c,k',b')\)。对于 \(l \le i\le r\),令 \(k_i \leftarrow k_ic + k',\ b_i \leftarrow b_ic + b'\)。
- \((l,r)\)。询问 \(max_{l\le i \le r} b_i\)。
本做法改进自此贴。
这一问题先前有所部分出现,而先前基本上只给出了 \(\mathcal O(n + (m + q)\sqrt n)\) 复杂度的分块做法。我们将看到,若保证 \(x > 0\),KTT 可以在 \(\mathcal O((n+ m\log n) \log^2n + q\log n)\) 的复杂度内完成所有操作。
同时为了叙述主要思想,这里仅考虑 \(c > 0\) 的情况。
思路简单:我们注意到操作 \(2\) 是可合并的,且操作 \(1\) 本质上就是对 KTT 上某个节点的子树增加 \(x\),而不是像是原始的 KTT 在全局增加 \(x\)。因此线段树上打 lazy 标记,表示操作 \(2\) 的累计效果和操作 \(1\) 的 \(x\) 的累计。同时在每个节点维护当前取到最大值的函数,以及 \(x\) 切换到下一个函数的阈值。
操作 \(2\) 不会更改最大值等。我们只需要考虑操作 \(1\) 的情况。操作 \(1\) 如果没有增加到阈值就在这里结束,反之切换函数并递归向子树。我们现在需要分析递归的总时间复杂度。
4.1.1 复杂度分析
考虑势能分析。
我们定义一个非叶子节点组成的集合 \(\mathcal P\),\(v\in \mathcal P\) 当且仅当 \(v\) 节点保存的取值最大的叶子对应斜率严格小于另一个孩子。记 \(d(i)\) 为节点 \(i\) 在线段树上的深度,并 \(d(1) = 1\)。定义势能函数 \(\Phi = \sum_{v \in \mathcal P} d(v)\)。
考虑操作 \(1\) 的最坏情况,即对每个节点的更新操作是单次进行的,那么我们定义一次更新操作的代价为 \(1\),均摊代价是 \(1 + \Delta\Phi\)。对于被修改的 \(v\),我们能知道修改后 \(v\) 必定不存在,但父亲可能加入,因此单次操作改变的势能 \(\le 1 - d(v) + d(fa_v) = 0\)。
考虑修改。当更改某个节点的 lazy 标记或其子树内的新值被上提到该位置时,该节点的信息被修改,其父亲的信息也可能在之后被修改,可能由不在 \(\mathcal P\) 中变为在 \(\mathcal P\) 中。这最多会导致势能增加 \(d(fa) = \mathcal O(\log n)\),一次操作会影响 \(\mathcal O(\log n)\) 个节点,因此总势能最多增加 \(\mathcal O(\log^2 n)\)。
初始势能为 \(\mathcal O(n \log n)\),单个势能贡献的复杂度是 \(\mathcal O(\log n)\),因此复杂度有上界 \(\mathcal O((n + m\log n)\log^2 n + q\log n)\)。
4.1.2 特殊情况
在诸如“区间加正公差的等差数列”问题中,我们能发现,序列中的 \(k\) 是单调递增的。因此所引发的更新操作必定是一个节点所取到的最大值位置从左儿子的贡献切换到右儿子,切换到右儿子后就再也不会被切换了。
因此直接设势能函数为 $\Phi = $ 最大值函数位于左子树的点数。操作过程中最多引发 \(\mathcal O(n + m\log n)\) 次操作,因此总时间复杂度为 \(\mathcal O((n + m\log n)\log n + q\log n)\)。
1. 3. 操作的代码(CF1178G)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define eb emplace_back
#define rep(i,a,b) for (register int i = (a), i##_ = (b) + 1; i < i##_; ++i)
#define pre(i,a,b) for (register int i = (a), i##_ = (b) - 1; i > i##_; --i)
const int N = 2e5 + 10; typedef long long ll;
const ll inf = 1e18;
int n, q, p, a[N], b[N], typ, t1, t2;
vector<int> g[N];
int bg[N], ed[N], idfn[N], stp;
void dfs(int u) {
bg[u] = ++ stp, idfn[stp] = u;
for (auto v : g[u]) {
a[v] += a[u];
b[v] += b[u];
dfs(v);
} ed[u] = stp;
}
struct KTT {
#define ls (p << 1)
#define rs (p << 1 | 1)
#define va(p) seg[p].a
#define vb(p) seg[p].b
#define ths(p) seg[p].thres
#define lzy(p) seg[p].lzy
struct node {
int a, b, thres, lzy;
} seg[N << 3];
int tmp_ths;
void ps_p(int p) {
if (va(ls) * vb(ls) < va(rs) * vb(rs)) va(p) = va(rs), vb(p) = vb(rs);
else va(p) = va(ls), vb(p) = vb(ls);
ths(p) = min(ths(ls), ths(rs));
if (vb(ls) != vb(rs) and (tmp_ths = (va(rs) * vb(rs) - va(ls) * vb(ls)) / (vb(ls) - vb(rs))) >= 0) {
ths(p) = min(ths(p), tmp_ths);
} lzy(p) = 0;
}
void ps_d(int p) {
if (!lzy(p)) return;
lzy(ls) += lzy(p), lzy(rs) += lzy(p);
ths(ls) -= lzy(p), va(ls) += lzy(p);
ths(rs) -= lzy(p), va(rs) += lzy(p);
lzy(p) = 0;
}
void build(int p = 1, int l = 2, int r = n << 1 | 1) {
if (l == r) {
va(p) = a[idfn[l >> 1]];
vb(p) = b[idfn[l >> 1]];
if (l & 1) vb(p) = -vb(p);
ths(p) = inf, lzy(p) = 0;
return ;
} int mid = l + r >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
ps_p(p);
}
void upd(int p, int l, int r, int L, int R, int v) {
if (L <= l and r <= R and v <= ths(p)) {
lzy(p) += v, ths(p) -= v, va(p) += v;
return ;
} int mid = l + r >> 1; ps_d(p);
if (L <= mid) upd(ls, l, mid, L, R, v);
if (mid < R) upd(rs, mid + 1, r, L, R, v);
ps_p(p);
}
int qry(int p, int l, int r, int L, int R) {
if (L <= l and r <= R) return va(p) * vb(p);
int mid = l + r >> 1, ret = - inf; ps_d(p);
if (L <= mid) ret = max(ret, qry(ls, l, mid, L, R));
if (mid < R) ret = max(ret, qry(rs, mid + 1, r, L, R));
return ret;
}
} Tr;
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> q;
rep(i,2,n) cin >> p, g[p].eb(i);
rep(i,1,n) cin >> a[i];
rep(i,1,n) cin >> b[i];
dfs(1), Tr.build();
while ( q-- ) {
cin >> typ >> t1;
if (typ == 1) cin >> t2, Tr.upd(1, 2, n << 1 | 1, bg[t1] << 1, ed[t1] << 1 | 1, t2);
else cout << Tr.qry(1, 2, n << 1 | 1, bg[t1] << 1, ed[t1] << 1 | 1) << '\n';
}
}
4.2 例题
两道水字数的题先咕着。CF573E和ROI2018 D1T3。
4.2.1 最大连续子段和
对于一个序列 \(a\),需要支持区间加整数,询问区间最大子段和。
首先考虑没有修改操作的做法。我们在线段树的每个节点 \(p\) 上维护 \(sum(p), lmax(p), rmax(p), tmax(p)\)。记左儿子为 \(ls\),右儿子为 \(rs\)。值满足
我们将这四个信息表为四个一次函数。沿用 4.1.1 处设计势能函数的思路,我们应当将子树内斜率大于该位置的直线的数量加入其中。我们称某个位置的最大值被更改为一次击败。当某个位置出现击败后,可能使得父亲被击败。
当一个节点发生了 \(lmax\) / \(rmax\) 的击败时,可能使得自己比父亲的 \(lmax\) / \(rmax\) 和 \(tmax\) 大,而发生了 \(tmax\) 的击败时只会使得自己比父亲的 \(tmax\) 大。
我们记 rank 值 \(r(p,kind)\) 如下:
- \(r(p, lmax / rmax)\):记 \(kind(p) = \max(a(p), b(p))\),rank 即为 \(a,b\) 中斜率大于 \(f\) 的直线数 \(\times d(p)^2\)。
- \(r(p, tmax)\):记 \(tmax(p) = \max(a(p), b(p), c(p))\),rank 即为 \(a,b,c\) 中斜率大于 \(f\) 的直线数 \(\times d(p)\)。
随后定义势能函数 \(\Phi = \sum_{p}\sum_{kind}r(p, kind)\)。
发生了一次 \(lmax\) / \(rmax\) 的击败,可能使得自己比父亲的 \(lmax\) / \(rmax\) 和 \(tmax\) 大,因此 \(\Delta\Phi\le 1 - d(p)^2 + (d(p) - 1) ^ 2 + (d - 1) = 1 - d \le 0\)。
发生了一次 \(tmax\) 的击败,显然有 \(\Delta\Phi\le 1 - d + (d - 1) = 0\)。
初始 \(lmax\) / \(rmax\) 会为势能贡献 \(\mathcal O(n\log^2 n)\),\(tmax\)为势能贡献 \(\mathcal O(n\log n)\)。
单次击败 \(lmax\) / \(rmax\) 的势能增加 \(\mathcal O(\log^3 n)\),\(tmax\) 的势能增加 \(\mathcal O(\log^2 n)\)。
因此总时间复杂度为 \(\mathcal O((n + m\log n)\log^3 n + q\log n)\)。
关于本题还可以使用性质分析得到更小的上界。
类比 4.1.2 的分析,我们关注 \(lmax\) / \(rmax\) 的变化。注意到 \(lmax\) 的决策点是单增的,因此 \(lmax\) / \(rmax\) 的总击败次数是 \(\mathcal O(n\log n)\) 的,贡献给 \(tmax\) 的势能就是 \(\mathcal O(n\log^2 n)\),就能得到 \(\mathcal O((n+ m)\log^3 n + q\log n)\)。
更紧的上界。但如果没有转移点单调往右的性质就只能到四只 \(\log\) 了。
感觉常数没有很大。
4.2.2 henry_y 的数列
对于一个序列 \(A\),支持形如 \((l,r,a,b,c)\) 的修改,即对于 \(l\le i\le r\),使 \(A_i \leftarrow A_i + ai^2 + bi + c\),保证 \(a,b \ge 0\)。并支持区间查询最小值。
这一问题中看似有 \(ai^2 + bi + c\),但其实 \(i\) 只与下标相关,因此实际上更类似于二元一次函数的最值问题。我们将 \(i, i^2\) 看作系数,将 \(a,b\) 看作变量。由于保证了 \(a,b\ge 0\),最小值位置单调左移。
仍然使用 KTT 的思想,考虑使用线段树维护当前最值,并维护加入 \(ai^2 + bi\) 使得最小值发生击败的阈值。
假设对于一个节点,左子树的最值为 \(A_i\),右子树的最值为 \(A_j\)。若 \(A_i > A_j\),则未来在该节点可能有 \(A_i\) 击败增大后的 \(A_j\)。这需要满足如下条件:
你一看这是个半平面的形式。也就是说,当 \((a,b)\) 进入一个半平面时发生击败。但是反过来想,如果没有进入就还没有切换。如果当前 \((a,b)\) 不会发生最值的击败,当且仅当 \((a,b)\) 落在子树的半平面交中。注意到斜率 \(i+j\) 导出了左子树任意半平面交的斜率小于右子树。因此我们采用可持久化平衡树维护半平面交,合并时二分即可做到 \(\mathcal O(\log n)\)。
考虑利用 4.1.2 的复杂度证明方法,我们设 $\Phi = $ 最大值在右子树中取得的节点个数。操作过程仍然只会引发 \(\mathcal O(n + m\log n)\) 次击败,每次击败会使势能增加一条链的 \(\mathcal O(\log n)\),即单次操作的势能为 \(\mathcal O(\log^2 n)\)。复杂度的上界是 \(\mathcal O((n + m \log n)\log^2 n + q\log n)\)。
该算法目前似乎没有实现。
这样我们就通过了 \(DS(n,s)\) 序列的优秀上界得到了一系列 \(\log\) 算法。
以下是博客签名,与正文无关。
请按如下方式引用此页:
本文作者 joke3579,原文链接:https://www.cnblogs.com/joke3579/p/paperessay221202.html。
遵循 CC BY-NC-SA 4.0 协议。
请读者尽量不要在评论区发布与博客内文完全无关的评论,视情况可能删除。