2022.10.13 闲话
还是觉得 artalter 是莫反大仙,我给他一道题,他没注意到一个 Key Observation 然后硬干干出一个复杂度相同的做法来 .
发现有弦图高论可以从 KMP 的 next 数组计数原字符串 .
下面是 copy 的 EI 队长论文《浅谈函数最值的动态维护》 .
how EI's mind works?
概述
问题描述
对于函数列 ,支持对于函数列进行一些修改,以及整体查询最值,即给一个 ,求
很自然的,可以想到维护 ,它是一个分段函数 .
考虑计算 最多分多少段,只考虑任意两个函数,如果段数不超过 ,其中 是次数,即可得到 的段数的一个较紧的界 .
则考虑分段函数每一段属于哪个函数,记作序列 ,当然下标从 开始,这样如果存在 使得 中存在长度超过 的 交替子序列,则 的段数必然超过 .
那么可以引入 DS 序列:
Davenport–Schinzel 序列
定义
一个序列 是一个 阶 Davenport–Schinzel 序列(简记作 序列),当且仅当 且 :
- 中任意相邻两项值不同 .
- 对于任意 ,任何 构成的序列如果是 的子序列,则长度不超过 .
定理:
定理
记 为 序列可能的最长长度,则有
其中 是反 Ackermann 函数 .
这个定理的证明十分有难度,这里就不再展开,可以从 EI 对于 的证明中窥得分晓 .
于是可知 对于任意常数 都是近线性的 .
这样段数就近乎是线性的了 .
与问题的联系
根据前面的成果,可以得到 个最高 次多项式的上包络线分段长度不超过 .
令分段函数在定义域外的部分,值都为 ,则可以得到任意两个分段 次函数是 阶交替的(考虑定义域有交,则定义域中贡献 阶,边界贡献 阶).
则 个分段 次函数最值上包络线分段长度不超过 .
下面有两种问题得到高效解决:
-
不修改函数,只维护集合 .
简要提一下做法:静态可以直接分治合并,询时二分,则预处理 ,询问 .
如果要支持加函数就用经典的二进制分组,这样询问就是 ,如果用分散层叠优化二分就还是 .
-
询问点单调递增,这样可以通过类似 Segment Tree Beats! 的思路设计,下面具体描述 .
KTT 解决询问点单调递增的情况
问题描述
对于 个函数组成的函数列 ,对于单调递增的 序列中的每个元素 ,求整体的最值,即
接下来的讨论中,假设处理的函数族是 阶交替且可以在常数时间求出交替位置的函数 .
Kinetic Tournament Tree (KTT)
其实就是 Segment Tree Beats! 的思路套上这个 Davenport–Schinzel 序列的分析 .
用线段树维护区间中对于当前 取到最大值的函数是哪个, 变化的时候暴力修改 .
每个点的分段个数是 ,线段树有 层,暴力 DFS 修改又要一个 ,于是时间复杂度 .
单点修改
这个 KTT 可以直接推广到支持单点修改函数序列 .
由于 单调递增,所以一个位置仍然可以看做只有一个函数,只不过这个函数分段。于是复杂度变为 .
势能分析表出线性情况的时间复杂度
问题描述
对于序列 ,,有三种操作:
- 给 ,对于 ,令 .
- 给 ,对于 ,令 , .
- 给 ,查询 .
约定: .
基本操作
相当于对于每个点维护一个函数 ,操作 1 就是区间 增加,而且注意到被操作 2 完全覆盖的区间可以发现 的大小关系都没有变化 .
于是考虑随便两个点 和 ,其中 , 同时经过操作 2 后会发生什么 . 可以发现 至少需要增加 才会使得大小关系改变,也就是说修改完之后大小关系是不会改变的!
于是考虑线段树维护区间的 什么变化,以及最大值在哪里取到,以及 再整体增加多少就会使得最大值切换 .
主要的问题时操作 1 时如果要切换最大值需要递归下去换,这个的复杂度分析如下 .
时间复杂度分析
根据前面的讨论,注意到切换最大值的时候一定是从斜率小的换成斜率大的 .
定义一个非叶子节点组成的集合 ,对于一个非叶子节点 ,如果 保留目前取值较大的儿子所对于的直线斜率严格小于另一个儿子,则 .
定义势函数 ,其中 是点 在线段树上的深度,根节点的深度为 (根节点深度其实不怎么重要,但是为了严密还是提一下).
于是一次切换 至少减小 ,因为一次对 的切换会使得 必然减少 ,并有可能增加 ,也就是至少减小 .
那么考虑两种操作只有被访问的节点可能加入 ,于是因为只会访问 个点于是只会增加 的势能 .
加上初始的 势能,可以得到总复杂度为 .
单调递增的情况
注意到此时切换只能从左儿子切换到右儿子 . 另外,对于一个点,如果它子树的最大值已经在右儿子了,那么打标记的时候即使左儿子被切换了,它也不会被切换,于是可以直接设 为最大值位于左子树的点数,从而比上面少一个 log .
时间复杂度 .
只加正数的第六分块
问题描述
维护一个整数序列 ,支持:
- 给 ,对于所有 ,令 .
- 给 ,询问区间 的最大子段和(可以为空).
操作 1 中所有 .
先放份代码实现,免得有些 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;
}
基本操作
我们有一个非常经典的维护最大子段和的分治做法(小白逛公园),维护区间和 ,前缀最大子段和 ,后缀最大子段和 ,和真正的最大子段和 .
转移即为
全都是关于区间加的一次函数 .
这样根据前面的线性情况做法,类似的去做就好了 .
其实 KTT 部分还是一致的,问题在于势能分析 .
时间复杂度分析
根据人类智慧,构造势函数的方法如下:
令 表示:
- 对于 和 ,对于该变量的决定式 , 表示当前 中斜率大于 的斜率的直线数量乘 .
- 对于 ,对于该变量的决定式 , 表示当前 中斜率大于 的斜率的直线数量乘 .
定义同上 .
P.S. 所谓「决定式」其实就是前面的转移抽象化一下 .
定义势函数 .
于是考虑一些操作对势能的贡献:
- 初始势能:给 贡献势能 ,给 贡献势能 .
- 一次修改:给 贡献势能 ,给 贡献势能 .
再根据人类智慧,发现 转移点单调不降,于是 的总切换次数就是 的,贡献给 的势能就是 的了 .
于是时间复杂度为 .
以下是博客签名,正文无关
本文来自博客园,作者:yspm,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/16789485.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】