2022.10.13 闲话

还是觉得 artalter 是莫反大仙,我给他一道题,他没注意到一个 Key Observation 然后硬干干出一个复杂度相同的做法来 .

发现有弦图高论可以从 KMP 的 next 数组计数原字符串 .

下面是 copy 的 EI 队长论文《浅谈函数最值的动态维护》 .

how EI's mind works?

概述

问题描述

对于函数列 fi:RR,支持对于函数列进行一些修改,以及整体查询最值,即给一个 x,求

maxj=1nfj(x)

很自然的,可以想到维护 F(x)=maxj=1nfj(x),它是一个分段函数 .

考虑计算 F(x) 最多分多少段,只考虑任意两个函数,如果段数不超过 s+1,其中 s 是次数,即可得到 F(x) 的段数的一个较紧的界 .

则考虑分段函数每一段属于哪个函数,记作序列 {am},当然下标从 0 开始,这样如果存在 x,y 使得 {a} 中存在长度超过 s+1x,y 交替子序列,则 max{fx,fy} 的段数必然超过 s+1 .

那么可以引入 DS 序列:

Davenport–Schinzel 序列

定义

一个序列 {σm} 是一个 (n,s) 阶 Davenport–Schinzel 序列(简记作 DS(n,s) 序列),当且仅当 σi[1,n]Z 且 :

  • σ 中任意相邻两项值不同 .
  • 对于任意 xy,任何 x,y 构成的序列如果是 σ 的子序列,则长度不超过 s+1 .

定理:

定理

λs(n)DS(n,s) 序列可能的最长长度,则有

λs(n)={ns=12n1s=22nα(n)+O(n)s=3Θ(n2α(n))s=4Θ(nα(n)2α(n))s=5n2α(n)t/t!+O(α(n)t1)s6where t=s22

其中 α(n) 是反 Ackermann 函数 .

这个定理的证明十分有难度,这里就不再展开,可以从 EI 对于 λ3(n)=O(nα(n)) 的证明中窥得分晓 .

于是可知 λs(n) 对于任意常数 s 都是近线性的 .

这样段数就近乎是线性的了 .

与问题的联系

根据前面的成果,可以得到 n 个最高 s 次多项式的上包络线分段长度不超过 λs(n) .

令分段函数在定义域外的部分,值都为 ,则可以得到任意两个分段 s 次函数是 s+2 阶交替的(考虑定义域有交,则定义域中贡献 s 阶,边界贡献 2 阶).

n 个分段 s 次函数最值上包络线分段长度不超过 λs+2(n) .

下面有两种问题得到高效解决:

  1. 不修改函数,只维护集合 .

    简要提一下做法:静态可以直接分治合并,询时二分,则预处理 Θ(λs(n)logn),询问 Θ(mlogn) .

    如果要支持加函数就用经典的二进制分组,这样询问就是 Θ(nlog2n),如果用分散层叠优化二分就还是 Θ(mlogn) .

  2. 询问点单调递增,这样可以通过类似 Segment Tree Beats! 的思路设计,下面具体描述 .

KTT 解决询问点单调递增的情况

问题描述

对于 n 个函数组成的函数列 fi:RR,对于单调递增的 {xq} 序列中的每个元素 xk,求整体的最值,即

maxj=1nfj(xk)

接下来的讨论中,假设处理的函数族是 s 阶交替且可以在常数时间求出交替位置的函数 .

Kinetic Tournament Tree (KTT)

其实就是 Segment Tree Beats! 的思路套上这个 Davenport–Schinzel 序列的分析 .

用线段树维护区间中对于当前 x 取到最大值的函数是哪个,x 变化的时候暴力修改 .

每个点的分段个数是 λs(n),线段树有 Θ(logn) 层,暴力 DFS 修改又要一个 log,于是时间复杂度 Θ(λs(n)log2n+q) .

单点修改

这个 KTT 可以直接推广到支持单点修改函数序列 .

由于 {x} 单调递增,所以一个位置仍然可以看做只有一个函数,只不过这个函数分段。于是复杂度变为 Θ(λs+2(n+m)log2n+q) .

势能分析表出线性情况的时间复杂度

问题描述

对于序列 {k}{b},有三种操作:

  1. l,r,x,对于 lir,令 bikix+bi .
  2. l,r,c,k,b,对于 lir,令 kicki+kbicbi+b .
  3. l,r,查询 maxi[l,r]bi .

约定:x,ki,bi,c>0 .

基本操作

相当于对于每个点维护一个函数 fi(x)=kix+bi,操作 1 就是区间 x 增加,而且注意到被操作 2 完全覆盖的区间可以发现 k,b 的大小关系都没有变化 .

于是考虑随便两个点 k1x+b1k2x+b2,其中 bi>b2k1<k2 同时经过操作 2 后会发生什么 . 可以发现 x 至少需要增加 b1b2k2k1 才会使得大小关系改变,也就是说修改完之后大小关系是不会改变的!

于是考虑线段树维护区间的 x,k,b 什么变化,以及最大值在哪里取到,以及 x 再整体增加多少就会使得最大值切换 .

主要的问题时操作 1 时如果要切换最大值需要递归下去换,这个的复杂度分析如下 .

时间复杂度分析

根据前面的讨论,注意到切换最大值的时候一定是从斜率小的换成斜率大的 .

定义一个非叶子节点组成的集合 P,对于一个非叶子节点 u,如果 u 保留目前取值较大的儿子所对于的直线斜率严格小于另一个儿子,则 uP .

定义势函数 Φ=uPdepth(u),其中 depth(u) 是点 u 在线段树上的深度,根节点的深度为 1(根节点深度其实不怎么重要,但是为了严密还是提一下).

于是一次切换 Φ 至少减小 1,因为一次对 u 的切换会使得 Φ 必然减少 depth(u),并有可能增加 depth(father(u)),也就是至少减小 1 .

那么考虑两种操作只有被访问的节点可能加入 P,于是因为只会访问 O(logn) 个点于是只会增加 O(log2n) 的势能 .

加上初始的 O(nlogn) 势能,可以得到总复杂度为 O(nlog2n+mlog3n+qlogn) .

{k} 单调递增的情况

注意到此时切换只能从左儿子切换到右儿子 . 另外,对于一个点,如果它子树的最大值已经在右儿子了,那么打标记的时候即使左儿子被切换了,它也不会被切换,于是可以直接设 Φ 为最大值位于左子树的点数,从而比上面少一个 log .

时间复杂度 O(nlogn+mlog2n+qlogn) .

只加正数的第六分块

问题描述

维护一个整数序列 {a},支持:

  1. l,r,x,对于所有 lir,令 aiai+x .
  2. 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 .

转移即为

sum=ls.sum+rs.sumlmax=max(ls.lmax,ls.sum+rs.lmax)rmax=max(rs.rmax,rs.sum+ls.rmax)totmax=max(ls.totmax,rs.totmax,ls.rmax+rs.lmax)

全都是关于区间加的一次函数 .

这样根据前面的线性情况做法,类似的去做就好了 .

其实 KTT 部分还是一致的,问题在于势能分析 .

时间复杂度分析

根据人类智慧,构造势函数的方法如下:

r(u,para) 表示:

  • 对于 r(u,lmax)r(v,rmax),对于该变量的决定式 f(x)=max{a(x),b(x)}r(u,para) 表示当前 a,b 中斜率大于 f 的斜率的直线数量乘 depth(u)2 .
  • 对于 r(u,totmax),对于该变量的决定式 f(x)=max{a(x),b(x),c(x)}r(u,para) 表示当前 a,b,c 中斜率大于 f 的斜率的直线数量乘 depth(u) .

depth(u) 定义同上 .

P.S. 所谓「决定式」其实就是前面的转移抽象化一下 .

定义势函数 Φ=uparar(u,para) .

于是考虑一些操作对势能的贡献:

  • 初始势能:给 lmax 贡献势能 O(nlog2n),给 totmax 贡献势能 O(nlogn) .
  • 一次修改:给 lmax 贡献势能 O(log3n),给 totmax 贡献势能 O(log2n) .

再根据人类智慧,发现 lmax 转移点单调不降,于是 lmax,rmax 的总切换次数就是 O(nlogn) 的,贡献给 totmax 的势能就是 O(nlog2n) 的了 .

于是时间复杂度为 O((n+q)log3n) .

posted @   yspm  阅读(148)  评论(9编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
😅​
点击右上角即可分享
微信分享提示