2022.10.13 闲话
还是觉得 artalter 是莫反大仙,我给他一道题,他没注意到一个 Key Observation 然后硬干干出一个复杂度相同的做法来 .
发现有弦图高论可以从 KMP 的 next 数组计数原字符串 .
下面是 copy 的 EI 队长论文《浅谈函数最值的动态维护》 .
how EI's mind works?
概述
问题描述
对于函数列 \(f_i:\R\to \R\),支持对于函数列进行一些修改,以及整体查询最值,即给一个 \(x\),求
\[\max_{j=1}^nf_j(x) \]
很自然的,可以想到维护 \(\displaystyle F(x)=\max_{j=1}^nf_j(x)\),它是一个分段函数 .
考虑计算 \(F(x)\) 最多分多少段,只考虑任意两个函数,如果段数不超过 \(s+1\),其中 \(s\) 是次数,即可得到 \(F(x)\) 的段数的一个较紧的界 .
则考虑分段函数每一段属于哪个函数,记作序列 \(\{a_m\}\),当然下标从 \(0\) 开始,这样如果存在 \(x,y\) 使得 \(\{a\}\) 中存在长度超过 \(s+1\) 的 \(x,y\) 交替子序列,则 \(\max\{f_x,f_y\}\) 的段数必然超过 \(s+1\) .
那么可以引入 DS 序列:
Davenport–Schinzel 序列
定义
一个序列 \(\{\sigma_m\}\) 是一个 \((n,s)\) 阶 Davenport–Schinzel 序列(简记作 \(\operatorname{DS}(n,s)\) 序列),当且仅当 \(\sigma_i\in[1,n]\cap\Z\) 且 :
- \(\sigma\) 中任意相邻两项值不同 .
- 对于任意 \(x\neq y\),任何 \(x,y\) 构成的序列如果是 \(\sigma\) 的子序列,则长度不超过 \(s+1\) .
定理:
定理
记 \(\lambda_s(n)\) 为 \(\operatorname{DS}(n,s)\) 序列可能的最长长度,则有
\[\lambda_s(n)=\begin{cases}n&s=1\\2n-1&s=2\\2n\alpha(n)+O(n)&s=3\\\Theta(n2^{\alpha(n)})&s=4\\\Theta(n\alpha(n)2^{\alpha(n)})&s=5\\n2^{\alpha(n)^t/t!+O(\alpha(n)^{t-1})}&s\ge 6&\text{where }t=\left\lfloor\dfrac{s-2}2\right\rfloor\\\end{cases} \]其中 \(\alpha(n)\) 是反 Ackermann 函数 .
这个定理的证明十分有难度,这里就不再展开,可以从 EI 对于 \(\lambda_3(n)=O(n\alpha(n))\) 的证明中窥得分晓 .
于是可知 \(\lambda_s(n)\) 对于任意常数 \(s\) 都是近线性的 .
这样段数就近乎是线性的了 .
与问题的联系
根据前面的成果,可以得到 \(n\) 个最高 \(s\) 次多项式的上包络线分段长度不超过 \(\lambda_s(n)\) .
令分段函数在定义域外的部分,值都为 \(-\infty\),则可以得到任意两个分段 \(s\) 次函数是 \(s+2\) 阶交替的(考虑定义域有交,则定义域中贡献 \(s\) 阶,边界贡献 \(2\) 阶).
则 \(n\) 个分段 \(s\) 次函数最值上包络线分段长度不超过 \(\lambda_{s+2}(n)\) .
下面有两种问题得到高效解决:
-
不修改函数,只维护集合 .
简要提一下做法:静态可以直接分治合并,询时二分,则预处理 \(\Theta(\lambda_s(n)\log n)\),询问 \(\Theta(m\log n)\) .
如果要支持加函数就用经典的二进制分组,这样询问就是 \(\Theta(n\log^2n)\),如果用分散层叠优化二分就还是 \(\Theta(m\log n)\) .
-
询问点单调递增,这样可以通过类似 Segment Tree Beats! 的思路设计,下面具体描述 .
KTT 解决询问点单调递增的情况
问题描述
对于 \(n\) 个函数组成的函数列 \(f_i:\R\to \R\),对于单调递增的 \(\{x_q\}\) 序列中的每个元素 \(x_k\),求整体的最值,即
\[\max_{j=1}^nf_j(x_k) \]
接下来的讨论中,假设处理的函数族是 \(s\) 阶交替且可以在常数时间求出交替位置的函数 .
Kinetic Tournament Tree (KTT)
其实就是 Segment Tree Beats! 的思路套上这个 Davenport–Schinzel 序列的分析 .
用线段树维护区间中对于当前 \(x\) 取到最大值的函数是哪个,\(x\) 变化的时候暴力修改 .
每个点的分段个数是 \(\lambda_s(n)\),线段树有 \(\Theta(\log n)\) 层,暴力 DFS 修改又要一个 \(\log\),于是时间复杂度 \(\Theta(\lambda_s(n)\log^2n+q)\) .
单点修改
这个 KTT 可以直接推广到支持单点修改函数序列 .
由于 \(\{x\}\) 单调递增,所以一个位置仍然可以看做只有一个函数,只不过这个函数分段。于是复杂度变为 \(\Theta(\lambda_{s+2}(n+m)\log^2n+q)\) .
势能分析表出线性情况的时间复杂度
问题描述
对于序列 \(\{k\}\),\(\{b\}\),有三种操作:
- 给 \(l,r,x\),对于 \(l\le i\le r\),令 \(b_i\gets k_ix+b_i\) .
- 给 \(l,r,c,k',b'\),对于 \(l\le i\le r\),令 \(k_i\gets ck_i+k'\),\(b_i\gets cb_i+b'\) .
- 给 \(l,r\),查询 \(\displaystyle\max_{i\in[l,r]}b_i\) .
约定:\(x,k_i,b_i,c>0\) .
基本操作
相当于对于每个点维护一个函数 \(f_i(x)=k_ix+b_i\),操作 1 就是区间 \(x\) 增加,而且注意到被操作 2 完全覆盖的区间可以发现 \(k,b\) 的大小关系都没有变化 .
于是考虑随便两个点 \(k_1x+b_1\) 和 \(k_2x+b_2\),其中 \(b_i>b_2\),\(k_1<k_2\) 同时经过操作 2 后会发生什么 . 可以发现 \(x\) 至少需要增加 \(\dfrac{b_1-b_2}{k_2-k_1}\) 才会使得大小关系改变,也就是说修改完之后大小关系是不会改变的!
于是考虑线段树维护区间的 \(x,k,b\) 什么变化,以及最大值在哪里取到,以及 \(x\) 再整体增加多少就会使得最大值切换 .
主要的问题时操作 1 时如果要切换最大值需要递归下去换,这个的复杂度分析如下 .
时间复杂度分析
根据前面的讨论,注意到切换最大值的时候一定是从斜率小的换成斜率大的 .
定义一个非叶子节点组成的集合 \(\mathcal P\),对于一个非叶子节点 \(u\),如果 \(u\) 保留目前取值较大的儿子所对于的直线斜率严格小于另一个儿子,则 \(u\in\mathcal P\) .
定义势函数 \(\displaystyle\Phi=\sum_{u\in\mathcal P}\operatorname{depth}(u)\),其中 \(\operatorname{depth}(u)\) 是点 \(u\) 在线段树上的深度,根节点的深度为 \(1\)(根节点深度其实不怎么重要,但是为了严密还是提一下).
于是一次切换 \(\Phi\) 至少减小 \(1\),因为一次对 \(u\) 的切换会使得 \(\Phi\) 必然减少 \(\operatorname{depth}(u)\),并有可能增加 \(\operatorname{depth}(\operatorname{father}(u))\),也就是至少减小 \(1\) .
那么考虑两种操作只有被访问的节点可能加入 \(\mathcal P\),于是因为只会访问 \(O(\log n)\) 个点于是只会增加 \(O(\log^2n)\) 的势能 .
加上初始的 \(O(n\log n)\) 势能,可以得到总复杂度为 \(O(n\log^2n+m\log^3n+q\log n)\) .
\(\{k\}\) 单调递增的情况
注意到此时切换只能从左儿子切换到右儿子 . 另外,对于一个点,如果它子树的最大值已经在右儿子了,那么打标记的时候即使左儿子被切换了,它也不会被切换,于是可以直接设 \(\Phi\) 为最大值位于左子树的点数,从而比上面少一个 log .
时间复杂度 \(O(n\log n+m\log^2 n+q\log n)\) .
只加正数的第六分块
问题描述
维护一个整数序列 \(\{a\}\),支持:
- 给 \(l,r,x\),对于所有 \(l\le i\le r\),令 \(a_i\gets a_i+x\) .
- 给 \(l,r\),询问区间 \([l,r]\) 的最大子段和(可以为空).
操作 1 中所有 \(x>0\) .
先放份代码实现,免得有些 gtm1514 说我口胡
Code
const int N = 444444;
const ll INF = 0x7f7f7f7f7f7f7f7fll;
int n, a[N], q;
struct line
{
int k; ll b;
line() = default;
line(int K, ll B) : k(K), b(B){}
line operator + (const line& rhs)const{return line(k + rhs.k, b + rhs.b);}
};
pair<line, ll> max(line a, line b)
{
if ((a.k < b.k) || ((a.k == b.k) && (a.b < b.b))) swap(a, b);
if (a.b >= b.b) return make_pair(a, INF);
return make_pair(b, (b.b - a.b) / (a.k - b.k));
}
struct SMT
{
#define ls (u << 1)
#define rs (u << 1 | 1)
#define mid ((l + r) >> 1)
struct Node
{
int l, r;
ll x, tag;
line sum, lmax, rmax, totmax;
Node() = default;
Node(line a, line b, line c, line d, ll e = INF, ll f = 0) : lmax(a), rmax(b), totmax(c), sum(d), x(e), tag(f){}
Node operator + (const Node& rhs) const
{
Node ans; ans.sum = sum + rhs.sum; ans.x = min(x, rhs.x);
auto _ = max(lmax, sum + rhs.lmax);
ans.lmax = _.first; chkmin(ans.x, _.second);
_ = max(rhs.rmax, rmax + rhs.sum);
ans.rmax = _.first; chkmin(ans.x, _.second);
_ = max(totmax, rhs.totmax);
chkmin(ans.x, _.second);
_ = max(_.first, rmax + rhs.lmax);
ans.totmax = _.first; chkmin(ans.x, _.second);
return ans;
}
Node& operator = (const Node& rhs)
{
x = rhs.x;
sum = rhs.sum; lmax = rhs.lmax; rmax = rhs.rmax; totmax = rhs.totmax;
return *this;
}
}tr[N << 2];
inline void pushup(int u){tr[u] = tr[ls] + tr[rs];}
inline void swit(int u, ll w)
{
if (w > tr[u].x)
{
ll t = w + tr[u].tag; tr[u].tag = 0;
swit(ls, t); swit(rs, t);
pushup(u);
return ;
}
tr[u].tag += w; tr[u].x -= w;
tr[u].lmax.b += tr[u].lmax.k * w;
tr[u].rmax.b += tr[u].rmax.k * w;
tr[u].totmax.b += tr[u].totmax.k * w;
tr[u].sum.b += tr[u].sum.k * w;
}
inline void pushdown(int u)
{
if (!tr[u].tag) return ;
swit(ls, tr[u].tag); swit(rs, tr[u].tag);
tr[u].tag = 0;
}
void build(int u, int l, int r)
{
tr[u].l = l; tr[u].r = r;
if (l == r){auto _ = line(1, a[l]); tr[u] = Node(_, _, _, _);return ;}
build(ls, l, mid); build(rs, mid+1, r);
pushup(u);
}
void change(int u, int L, int R, int x)
{
int l = tr[u].l, r = tr[u].r;
if (inrange(l, r, L, R)){swit(u, x); return ;}
pushdown(u);
if (L <= mid) change(ls, L, R, x);
if (R > mid) change(rs, L, R, x);
pushup(u);
}
Node query(int u, int L, int R)
{
int l = tr[u].l, r = tr[u].r;
if (inrange(l, r, L, R)) return tr[u];
pushdown(u);
if (R <= mid) return query(ls, L, R);
if (L > mid) return query(rs, L, R);
return query(ls, L, R) + query(rs, L, R);
}
#undef mid
#undef rs
#undef ls
}T;
int main()
{
scanf("%d%d", &n, &q);
for (int i=1; i<=n; i++) scanf("%d", a+i);
T.build(1, 1, n);
int opt, l, r, x;
while (q--)
{
scanf("%d%d%d", &opt, &l, &r);
if (opt == 1){scanf("%d", &x); T.change(1, l, r, x);}
else printf("%lld\n", max(0ll, T.query(1, l, r).totmax.b));
}
return 0;
}
基本操作
我们有一个非常经典的维护最大子段和的分治做法(小白逛公园),维护区间和 \(sum\),前缀最大子段和 \(lmax\),后缀最大子段和 \(rmax\),和真正的最大子段和 \(totmax\) .
转移即为
全都是关于区间加的一次函数 .
这样根据前面的线性情况做法,类似的去做就好了 .
其实 KTT 部分还是一致的,问题在于势能分析 .
时间复杂度分析
根据人类智慧,构造势函数的方法如下:
令 \(r(u,para)\) 表示:
- 对于 \(r(u,lmax)\) 和 \(r(v,rmax)\),对于该变量的决定式 \(f(x)=\max\{a(x),b(x)\}\),\(r(u,para)\) 表示当前 \(a,b\) 中斜率大于 \(f\) 的斜率的直线数量乘 \(\operatorname{depth}(u)^2\) .
- 对于 \(r(u,totmax)\),对于该变量的决定式 \(f(x)=\max\{a(x),b(x),c(x)\}\),\(r(u,para)\) 表示当前 \(a,b,c\) 中斜率大于 \(f\) 的斜率的直线数量乘 \(\operatorname{depth}(u)\) .
\(\operatorname{depth}(u)\) 定义同上 .
P.S. 所谓「决定式」其实就是前面的转移抽象化一下 .
定义势函数 \(\displaystyle\Phi=\sum_u\sum_{para}r(u,para)\) .
于是考虑一些操作对势能的贡献:
- 初始势能:给 \(lmax\) 贡献势能 \(O(n\log^2n)\),给 \(totmax\) 贡献势能 \(O(n\log n)\) .
- 一次修改:给 \(lmax\) 贡献势能 \(O(\log^3n)\),给 \(totmax\) 贡献势能 \(O(\log^2n)\) .
再根据人类智慧,发现 \(lmax\) 转移点单调不降,于是 \(lmax,rmax\) 的总切换次数就是 \(O(n\log n)\) 的,贡献给 \(totmax\) 的势能就是 \(O(n\log^2n)\) 的了 .
于是时间复杂度为 \(O((n+q)\log^3n)\) .
以下是博客签名,正文无关
本文来自博客园,作者:Jijidawang,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/16789485.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ