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)\) .

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

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

    简要提一下做法:静态可以直接分治合并,询时二分,则预处理 \(\Theta(\lambda_s(n)\log n)\),询问 \(\Theta(m\log n)\) .

    如果要支持加函数就用经典的二进制分组,这样询问就是 \(\Theta(n\log^2n)\),如果用分散层叠优化二分就还是 \(\Theta(m\log n)\) .

  2. 询问点单调递增,这样可以通过类似 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\}\),有三种操作:

  1. \(l,r,x\),对于 \(l\le i\le r\),令 \(b_i\gets k_ix+b_i\) .
  2. \(l,r,c,k',b'\),对于 \(l\le i\le r\),令 \(k_i\gets ck_i+k'\)\(b_i\gets cb_i+b'\) .
  3. \(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\}\),支持:

  1. \(l,r,x\),对于所有 \(l\le i\le r\),令 \(a_i\gets a_i+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\) .

转移即为

\[\begin{aligned} sum &= ls.sum + rs.sum\\ lmax &= \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) \end{aligned} \]

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

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

其实 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)\) .

posted @ 2022-10-13 20:09  Jijidawang  阅读(129)  评论(9编辑  收藏  举报
😅​