Slope Trick

原理

若一个函数满足:

  1. 连续
  2. 分段线性
  3. 凸性
    则称它是折线函数。折线函数可以使用 Slope Trick 来快速维护。

我们发现我们可以仅通过记录转折点,转折点处斜率变化,以及一侧的直线即可维护出整个函数。具体而言每一个转折点出现,我们就认为该函数在该点斜率变化了 1.
举个例子,对于如下函数:

\[y(x)=\left\{ \begin{aligned} -x & \quad x<0\\ 0 & \quad 0\leq x \leq 2\\ 2x + 3 & \quad x>2\\ \end{aligned} \right. \]

我们可以用 \([\{0, 2, 2\}, 2x + 3]\) 来表示。

\(f(x)\)\(g(x)\) 是两个凹凸性相同的满足要求的函数,则 \(h(x) = f(x) + g(x)\) 也是与它们凹凸性相同且满足要求的函数。同时 \(S_h = S_f \cup S_g, F_h = F_f + F_g\)

CF713C

题意

给定一个长为 \(n\) 的数组,每次操作可以把任意一个元素 \(+1\)\(-1\),求使得其变成严格递增的最小操作次数。
\(n \leq 10^5, a_i \leq 10^9\)

题解

由于值域比较大,直接 dp 不太能做。考虑设 \(b_i = a_i - i\),原问题即转化为求将 \(b\) 转化为非严格递增的最小操作次数。容易发现最终序列中的值一定都属于 \(b\) 序列。

\(f_{i, j}\) 表示 dp 到第 \(i\) 位,第 \(i\) 位值为 \(b_j\) 的最小操作次数。则 \(f_{i,j} = \min_{k \leq j} \{ f_{i - 1, k} \} + |b_i - b_j|\)
\(g_{i, j} = \min_{k \leq j} \{ f_{i, k} \}\),则 \(f_{i,j} = g_{i - 1, j} + |b_i - b_j|\)

\(F_i(x) = f_{i, j}, G_i(x) = g_{i, j}\)。容易发现 \(F_i(x)\)\(G_i(x)\) 是折线函数。具体证明考虑归纳法。

首先 \(F_0\)\(G_0\) 必定是凸的。当 \(G_{i - 1}\) 是凸的,其加上一个凹凸性相同的绝对值函数得到 \(F_i\) 也是凸的。具体原因考虑二阶导非负。对于 \(F_i\) 前缀取 \(\min\) 即得到 \(G_i\),所以 \(G_i\) 也是凸的。

现在考虑用 Slope Trick 来维护 \(F_i\)\(G_i\)。易得答案为 \(G_n\) 的无穷远处对应的值。

因为是前缀取 \(\min\) 所以 \(G_i\) 的最右侧函数的斜率为 \(0\)。且前面的函数与 \(F_i\) 重合。
初始时 \(G_0\) 恒为 0, 表示为 \([\emptyset, 0]\)。对 \(G_{i - 1}\),加上一个绝对值函数 \(|x - b_i|\) 即在转折点集合中插入点两次点 \(b_i\),并将最右端的函数斜率 \(+1\),即可转化成 \(F_i\)。再将 \(F_i\) 最右侧推平即可得到 \(G_i\)

现在来关注一下 \(G_i\) 无穷远点的值的变化,即答案的变化。因为最右侧斜率为 \(0\),所以无穷远点的答案等价于斜率为 0 的转折点处的答案。上一张图通俗的解释一下。

变化量即 \(f_r'(top) - f_r'(b_i)\),此题中即为 \(top - b_i\)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read() {
	int res = 0, f = 1; char ch;
	while((ch = getchar()) && (ch < '0' || ch > '9') && ch != '-');
	(ch == '-') ? f = -1 : res = ch - '0';
	while((ch = getchar()) && ch >= '0' && ch <= '9') res = (res << 3) + (res << 1) + ch - '0';
	return res * f;
}

const int MAXN = 1e5 + 5;

int n;
priority_queue<int> q;

signed main() {
	n = read();
	int res = 0;
	for(int i = 1, x; i <= n; i++) {
		x = read() - i;
		q.push(x); q.push(x);
		res += q.top() - x; q.pop();
	}
	printf("%lld\n",res);
	return 0;
}

ABC217H

题意

有一个游戏,发生在一条数轴上,最初 \(0\) 时刻,Snuke 在 \(0\) 号节点。

每过一个时刻,你可以选择向正方向或负方向移动一格,或者不移动。

接下来有 \(n\) 个事件,每一个事件用 \(T_i,D_i,X_i\) 描述,其中 \(T_i\) 表示事件发生时刻,假设 Snuke 此时在 \(p\) 点:

  • \(D_i=0\),Snuke 将会受到 \(\max\{0,X_i-p\}\) 的伤害。
  • \(D_i=1\),Snuke 将会受到 \(\max\{0,p-X_i\}\) 的伤害。

请问 \(n\) 次事件之后 Snuke 受到的伤害量的最小值。

  • \(1\le n\le 2\times 10^5\)
  • \(1\le T_1\le T_2\le \cdots\le T_n\le 10^9\)
  • \(\forall i\in[1,n],D_{i}\in\{0,1\},-10^9\le X_i\le 10^9\)

题解

\(H_i(x)\) 表示在 \(i\) 时刻位于 \(x\) 收到的伤害,设 \(f_{i, j}\) 表示在 \(i\) 时刻位于 \(j\) 受到的最小伤害和。
\(t = T_i - T_{i - 1}\),有 \(f_{i, j} = \min_{j - t \leq k \leq j + t} \{f_{i - 1, k}\} + H_i(j)\)

此题与上一题的差别在于函数取 \(\min\) 的是一段区间。换句话说,\(F_i\) 斜率为 0 的点位于中间,不存在某一侧的函数斜率为 0。

于是我们可以考虑从斜率为 \(0\) 的点的位置将函数剖成两半,\(L_i\)\(R_i\)。它们分别有一侧斜率为 0。

考虑如何转移。首先因为\(L_i\) 单调递减,\(R_i\) 单调递增,所以将 \(L\) 向左平移 \(t\),将 \(R\) 向右平移 \(t\)。具体实现整体打标机即可。

设斜率为 \(0\) 的段为 \([l, r]\),当前插入点为 \(x\),以 \(D_i = 0\) 为例。此时插入一个点等价于把它左侧所有点的斜率 \(-1\),分两种情况讨论。

  1. \(x \leq r\),此时插入 \(x\) 并不会改变答案。
  2. \(x > r\),此时插入 \(x\) 会对斜率为 0 的区间答案产生影响。具体而言,答案会增加 \(x - r\),斜率为 \(0\) 的点变为斜率为 \(-1\) 的区间,斜率为 \(1\) 的区间变为斜率为 \(0\) 的区间。所以把 \(R\) 的堆顶弹出压入到 \(L\) 中。

CF1534G

APIO2016 烟火表演

CTSC2009 序列变换

posted @ 2023-02-25 21:09  zym417  阅读(79)  评论(0编辑  收藏  举报