【总结】斜率优化 DP

于是,XSC062 开始写总结。

斜率优化 DP

前置芝士

单调队列优化 DP

正文

我们以一道题为例。

打印文章

双倍经验 | 三倍经验

Solution

明显 DP。

那么 DP 式就是:

\[\begin{aligned} f_i&=\min\{f_j+(s_i-s_j)^2+M\} \\ &=\min\{f_j+{s_i}^2-2\times s_i\times s_j+{s_j}^2+M\} \\ &=\min\{f_j-2\times s_i\times s_j+{s_j}^2\}+{s_i}^2+M \end{aligned} \]

其中 \(s\)\(c\) 的前缀和。

时间复杂度 \(\mathcal O(n^2)\),明显爆炸,所以我们需要优化。

在上一篇的 单调队列优化DP 中,我们提到过,只有 DP 式中的与 \(i\) 有关的项能直接提出来时,我们才能使用单调队列优化,而这里的 \(s_i\)\(s_j\) 相乘,无法使用单调队列优化。

我们思考,对于 \(f_i\) 来说,无非就是选出最优的 \(j\) 来构造它。

假设有 \(j\)\(k\),如何判断 \(j\)\(k\) 谁更优呢?

我们先钦定 \(j\) 优于 \(k\),且 \(j<k\)

那么可以得到:

\[f_j-2\times s_i\times s_j+{s_j}^2+{s_i}^2+M<f_k-2\times s_i\times s_k+{s_k}^2+{s_i}^2+M \]

化简得:

\[f_j-2\times s_i\times s_j+{s_j}^2<f_k-2\times s_i\times s_k+{s_k}^2 \]

再将\(j,k\) 有关的项放到左边,与 \(i\) 有关的项放到右边:

\[f_j-f_k+{s_j}^2-{s_k}^2<2\times s_i\times s_j-2\times s_i\times s_k \\ f_j-f_k+{s_j}^2-{s_k}^2<2\times s_i\times(s_j-s_k) \]

左右两边同时 \(\div\)\(i\) 无关的项 \(2\times(s_j-s_k)\)

\[\dfrac{(f_j+{s_j}^2)-(f_k+{s_k}^2)}{(2\times s_j)-(2\times s_k)}<s_i \]

如果满足上式 ,则 \(j<k\)\(j\) 优于 \(k\)

接下来是斜率优化的重点部分。

在义务教育阶段,学生学习了 一次函数,它的几何意义表示为一条直线,一次项的系数就是直线的斜率,只不过当直线与 \(x\) 轴垂直的时候无法表示。虽然没有明确给出斜率这个名词,但实际上思想已经渗透到其中。

直线对 \(x\) 轴的倾斜角 \(\alpha\) 的正切值 \(\tan\alpha\) 称为该直线的“斜率”,并记作 \(k\) ,公式为 \(k=\tan\alpha\)

\(k=\tan\alpha=\dfrac{\Delta y}{\Delta x}=\dfrac{y_2-y_1}{x_2-x_1}\)\(\dfrac{y_1-y_2}{x_1-x_2}\)

——选自 斜率_百度百科

XSC062 看完了百度百科表示 你 TM 在说些啥是不是欺负我六年义务教育的小学生 恍然大悟

上面推出来的那个关于 \(j\)\(k\) 的DP式,不就是求两个点 \((2\times s_j,f_j+{s_j}^2)\)\((2\times s_k,f_j+{s_k}^2)\) 连成一条线之后的坡度吗?

本文后面的部分,\(x\) 的含义会在「点\((2\times s_x,f_x+{s_x}^2)\) 」和「下标 \(x\) 」之间漂浮,请根据语境识别。

随后 XSC062 边打瞌睡边听 GM 讲课(特异功能),勉强算是明白了中心意思:

如图,假设有三个点 \(A,B,C\),以及 \(l_{AB}\) 的斜率 \(k_1\)\(l_{BC}\) 的斜率 \(k_2\)

上凸

我们暂且把这个向外凸起的奇怪玩意儿称为一个「上凸」。

回到前面的我们得到的那个结论:

\[\dfrac{(f_j+{s_j}^2)-(f_k+{s_k}^2)}{(2\times s_j)-(2\times s_k)}<s_i \]

如果满足上式 ,则 \(j<k\)\(j\) 优于 \(k\)

简单记忆为:

  • \(l_{NM}\) 的斜率比 \(s_i\) 小,则 \(N<M\)\(N\) 优于 \(M\)
  • 反之,若点 \(l_{NM}\) 的斜率比 \(s_i\) 大,则 \(M<N\)\(M\) 优于 \(N\)

\(A,B\) 代入,我们得知:

  1. \(k_1\leqslant s_i\),则 \(B\) 优于 \(A\)
  2. \(k_1>s_i\),则 \(A\) 优于 \(B\)
  3. \(k_2\leqslant s_i\),则 \(C\) 优于 \(B\)
  4. \(k_2>s_i\),则 \(B\) 优于 \(C\)

因为斜率 = 倾斜度 = 竖得有多高,明显 \(k_1>k_2\)。所以针对一个大小关系,\(k_1\)\(k_2\) 之间只有可能有:

  1. \(s_i<k_2<k_1\),此时 \(B\) 优于 \(C\)\(A\) 优于 \(B\)\(A\) 为最优解。
  2. \(k_2<s_i<k_1\),此时 \(C\) 优于 \(B\)\(A\) 优于 \(B\)\(A,C\) 为最优解。
  3. \(k_2<k_1<s_i\),此时 \(C\) 优于 \(B\)\(B\) 优于 \(A\)\(C\) 为最优解。

发现了吗?\(B\) 永远都不可能是最优解。

所以如果我们要维护一个最优解的序列,就可以不要 \(B\) 了。

上凸

那么,因为我们已经删掉了所有「上凸」的突起部分,所以这个最优解序列里肯定不会再存在「上凸」了。

如果是这样的「下凸」呢?

下凸

\(k_1\)\(l_{AB}\) 的斜率,\(k_2\)\(l_{BC}\) 的斜率。

\(A,B\) 代入,我们得知:

  1. \(k_1\leqslant s_i\),则 \(B\) 优于 \(A\)
  2. \(k_1>s_i\),则 \(A\) 优于 \(B\)
  3. \(k_2\leqslant s_i\),则 \(C\) 优于 \(B\)
  4. \(k_2>s_i\),则 \(B\) 优于 \(C\)

明显 \(k_1<k_2\)。所以 \(k_1\)\(k_2\) 之间只有可能有:

  1. \(s_i<k_1<k_2\),此时 \(B\) 优于 \(C\)\(A\) 优于 \(B\)\(A\) 为最优解。
  2. \(k_1<s_i<k_2\),此时 \(B\) 优于 \(C\)\(B\) 优于 \(A\)\(B\) 为最优解。
  3. \(k_1<k_2<s_i\),此时 \(C\) 优于 \(B\)\(B\) 优于 \(A\)\(C\) 为最优解。

所以,在下凸的情况中,三个点都有可能是最优解,都需要保留。

现在呢,所有上凸都被去掉了,只剩下凸,所以大概最后的最优解序列就长这个样子:

下凸包

下 秃 包

反过来看就是 lifan 的脑袋了

观察发现,斜率是从左往右递增的。

所以,我们考虑用单调队列来当这个「最优解序列」。

维护队头

即保证队头元素为最优解。

设队头为 \(q_l\)

如果 \(q_{l+1}\)\(q_l\) 形成的斜率 \(\leqslant s_i\),根据上面推出来的玩意儿,得到 \(q_{l+1}\) 优于 \(q_l\)

那还要 \(q_l\) 干啥,直接 l++

更新 DP 值

\(f_i=f_{q_l}-2\times s_i\times s_{q_l}+{s_{q_l}}^2+{s_i}^2+M\)

维护队尾

即保证里面塞的点相邻两个的斜率递增。

设队尾为 \(q_r\),我们要往最优解队列里 push 一个 \(i\)

若队尾两个点 \(q_r,q_{r-1}\) 形成的斜率比 \(i,q_r\) 形成的斜率大,那么push(i)后,整个队列的斜率就不再单调递增,所以此时要将r--。(因为 上一篇关于单调队列的博客 中讲到的 \(i\) 必须入队,只能委屈一下 \(q_r\) 了)

注意事项

众所周知,斜率是个浮点数。为了避免损失精度造成的一些惨案,我们交叉相乘,将分子、分母分开处理。

以及时刻都要保证队列中至少有两个点,因为要访问 \(q_l,q_{l+1}\)\(q_r,q_{r-1}\)

Code

#include <cstdio>
const int maxn = 5e5 + 5;
int n, m, l, r;
int f[maxn], c[maxn], q[maxn];
inline int getDP(int i, int j) {
	return f[j] + m + (c[i] - c[j]) * (c[i] - c[j]);
}
inline int getup(int j, int k){
	return f[j] + c[j] * c[j] - f[k] - c[k] * c[k]; // 计算分子的值
}
inline int getdown(int j, int k) {
	return (c[j] - c[k]) * 2; // 计算分母的值
}
int main(){
	while (~scanf("%d %d", &n, &m)) {
		l = r = 1; // 凡是涉及到前缀和的单调队列,队列中必须存在初始 0
		for (int i = 1; i <= n; ++i) {
			scanf("%d", &c[i]); // c 自给自足
			c[i] += c[i - 1];
			// # 维护队头
			// 注意此处的条件为 l < r,因为队列中至少需要存在两个点
			while (l < r && getup(q[l+1], q[l]) <= c[i] * getdown(q[l + 1],q[l]))
				l++;
			// # 转移状态
			f[i] = getDP(i, q[l]);
			// # 维护队尾
			while(l < r&& getup(i, q[r]) * getdown(q[r], q[r-1]) <= getup(q[r] ,q[r - 1]) * getdown(i, q[r]))
				r--;
			q[++r] = i;
		}
		printf("%d\n",f[n]);
	}
	return 0;
}

在对 DP 式变形时,我们最好将其化为 \(\dfrac{(j)-(k)}{(j)-(k)}<x\)\(\dfrac{(j)-(k)}{(j)-(k)}>x\) 的形式。

这个板子只适用于维护下凸包的情况。当中间的符号为 \(>\) 时,我们会在这份代码上稍作改动,维护一个上凸包,后文会提到有关内容。


玩具装箱

双倍经验 | 三倍经验

Solution

从今往后我们就只讲怎么推式子,不再证明下凸等性质了。

\(f_i\) 表示第 \(i\) 个玩具放完后的最小费用。

\[f_i=\min\{f_j+(i-j-1+\sum\limits_{k=i}^jC_k-L)^2\} \]

为了让这个式子好拆,我们在一开始让 l++,并且 \(C\) 再次自给自足,为输入的 \(C\) 的前缀和数组。

于是式子就变成:

\[f_i=\min\{f_j+(i-j-L+C_i-C_j)^2\} \]

明显硬拆会死人。(反正我试过,比较适合用来发泄

所以我们把式子变成这样:

\[f_i=\min\{f_j+((C_i+i)-(C_j+j)-L)^2\} \]

既然 \(C_i\)\(i\)\(C_j\)\(j\) 是对应的,那么直接预处理,给 \(C_i\) 加上 \(i\) 不就行了?

现在这个 \(C_i\) 的含义和实现就变得有点曲折难懂了。

具体实现如下:

for (int i = 1; i <= n; ++i) {
	scanf("%lld", &c[i]);
	c[i] += c[i - 1];
}
for (int i = 1; i <= n; ++i)
	c[i] += i;

也就是说,\(C_i\) 是在前缀和的基础上加了一个 \(i\),注意不能把 \(i\) 也一起前缀和了。

然后式子就变成了这样:

\[\begin{aligned} f_i&=\min\{f_j+(C_i-C_j-L)^2\} \\ &=\min\{f_j+{C_i}^2+{C_j}^2+L^2-2\times C_i\times C_j-2\times C_i\times L+2\times C_j\times L\} \\ &=\min\{f_j+{C_j}^2-2\times C_i\times C_j+2\times C_j\times L\}+{C_i}^2+L^2-2\times C_i\times L \end{aligned} \]

\(j\) 优于 \(k\)\(j<k\)

得:

\[f_j+{C_j}^2-2\times C_i\times C_j+2\times C_j\times L<f_k+{C_k}^2-2\times C_i\times C_k+2\times C_k\times L \\ f_j-f_k+{C_j}^2-{C_k}^2+2\times C_j\times L-2\times C_k\times L<2\times C_i\times C_j-2\times C_i\times C_k \\ f_j-f_k+{C_j}^2-{C_k}^2+2\times C_j\times L-2\times C_k\times L<2\times(C_j-C_k)\times C_i \\ \dfrac{f_j-f_k+{C_j}^2-{C_k}^2}{2\times(C_j-C_k)}<C_i \\ \dfrac{(f_j+{C_j}^2)-(f_k+{C_k}^2)}{(2\times C_j)-(2\times C_k)}<C_i \]

Code

#include <cstdio>
#define int long long
const int maxn = 5e5 + 5;
const int LEN = (1 << 20);
int n, m, h, t;
int c[maxn], q[maxn], f[maxn];
#define nec getchar
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1; ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
inline int getup(int j, int k) {
	return (f[j] + c[j] * c[j] + 2 * c[j] * m)
		   - (f[k] + c[k] * c[k] + 2 * c[k] * m);
}
inline int getdown(int j, int k) {
	return (2 * c[j]) - (2 * c[k]);
}
inline int getDP(int i, int j) {
	return f[j] + (c[i] - c[j] - m) * (c[i] - c[j] - m);
}
signed main() {
	read(n); read(m); h = t = 1; ++m;
	for (int i = 1; i <= n; ++i)
		read(c[i]), c[i] += c[i - 1];
	for (int i = 1; i <= n; ++i) {
		c[i] += i;
		while (h < t && getup(q[h + 1], q[h]) <
			   c[i] * getdown(q[h + 1], q[h])) ++h;
		f[i] = getDP(i, q[h]);
		while (h < t
			   && getup(i, q[t]) * getdown(q[t], q[t - 1]) <=
			   getdown(i, q[t]) * getup(q[t], q[t - 1])) --t;
		q[++t] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

任务安排 1

双倍经验 | 三倍经验

Solution

感谢蓝书。这里按着蓝书上的思维走。

解法一

暴力。

此处的 \(t,c\) 为输入的 \(t,c\) 的前缀和数组。

\(f_{i,j}\) 为前 \(i\) 个任务分成 \(j\) 批的最小费用。

\(S\times j+t_i\) 为第 \(i\) 个任务的完成时间。

得出状态转移方程(\(k\) 枚举上一批任务结束位置):

\[f_{i,j}=\min\limits_{0\leqslant k<i}\{f_{k,j-1}+(S\times j+t_i)\times(c_i-c_k)\} \]

时间复杂度 \(\mathcal O(n^3)\)

代码
#include <cstdio>
#include <cstring>
#define int long long
const int inf = 1e18;
const int maxn = 5005;
const int LEN = (1 << 20);
int f[maxn][maxn];
int n, s, ans = inf;
int t[maxn], c[maxn];
inline int min(int x, int y) { return x < y ? x : y; }
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
signed main() {
	read(n); read(s);
	memset(f, 0x3f, sizeof (f));
	for (int i = 1; i <= n; ++i) {
		read(t[i]); read(c[i]);
		t[i] += t[i - 1];
		c[i] += c[i - 1];
		f[i][1] = (s + t[i]) * c[i]; // 初始化
	}
	for (int i = 2; i <= n; ++i) {
		for (int j = 2; j <= i; ++j) {
			for (int k = 1; k < i; ++k)
				f[i][j] = min(f[i][j], f[k][j - 1] + (t[i] + s * j) * (c[i] - c[k]));
		}
	}
	for (int i = 1; i <= n; ++i)
		ans = min(ans, f[n][i]);
	printf("%lld", ans);
	return 0;
}

亲测 TLE 70

解法二

脑子炸了,想了好久才想明白这个优化的正确性。

思考,以上代码需要 \(j\) 这一维的根本原因是什么?

因为我们无法确定之前已经划分了多少批,也就是无法确定 \(S\) 的个数。

换个角度思考,我们无法确定之前,却可以确定之后。

什么意思呢?如果我们在任务 \(i\) 处划分,那么任务 \(i\) 以及任务 \(i\) 以后的所有任务的执行时间都会延后 \(S\)

因为 \(i\) 以后的状态也会使用 \(f_i\) 的值,我们在计算 \(f_i\) 时就将 \(S\) 提出来,提前把后面的 \(c\) 乘上不就行了?

中间的结果不对劲也无所谓,只要最后的答案是对的就行了。

也就是说,我们没有直接求出每批任务的完成时刻,而是在一批任务「开始」对后续任务产生影响时,就先把费用累加到答案中。这是一种名为「费用提前计算」的经典思想。

——李煜东《算法竞赛进阶指南》

状态转移方程:

\[f_i=\min\limits_{0\leqslant j<i}\{f_j+(c_i-c_j)\times t_i+s\times(c_n-c_j)\} \]

此处,\(f_i\) 没有具体含义。

代码
#include <cstdio>
#include <cstring>
#define int long long
const int maxn = 1e4 + 5;
const int LEN = (1 << 20);
int n, s;
int f[maxn], t[maxn], c[maxn];
inline int min(int x, int y) { return x < y ? x : y; }
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
signed main() {
	read(n); read(s);
	memset(f, 0x3f, sizeof (f));
	for (int i = 1; i <= n; ++i) {
		read(t[i]); read(c[i]);
		t[i] += t[i - 1];
		c[i] += c[i - 1];
	}
	f[0] = 0;
	for (int i = 1; i <= n; ++i) {
		for (int j = 0; j < i; ++j)
			f[i] = min(f[i], f[j] + t[i]* (c[i] - c[j]) + s * (c[n] - c[j]));
	}
	printf("%lld", f[n]);
	return 0;
}

任务安排 2

双倍经验

\(n\) 的范围变大了,\(n^2\) 过不了。

Solution

这不随手加个斜率优化的事儿吗。

我们继续瞎搞这个式子。

\[\begin{aligned} f_i&=\min\{f_j+(c_i-c_j)\times t_i+s\times(c_n-c_j)\} \\ &=\min\{f_j+c_i\times t_i-c_j\times t_i+s\times c_n-s\times c_j\} \\ &=\min\{f_j-c_j\times t_i-s\times c_j\}+c_i\times t_i+s\times c_n \end{aligned} \]

\(j\) 优于 \(k\)\(j<k\)

则有:

\[f_j-c_j\times t_i-s\times c_j<f_k-c_k\times t_i-s\times c_k \\ f_j-f_k-s\times c_j+s\times c_k<c_j\times t_i-c_k\times t_i \\ f_j-f_k-s\times c_j+s\times c_k<t_i\times(c_j-c_k) \\ \dfrac{(f_j-s\times c_j)-(f_k-s\times c_k)}{(c_j)-(c_k)}<t_i \]

然后就是老套路。

时间复杂度 \(\mathcal O(n)\)

Code

#include <cstdio>
#define int long long
const int maxn = 5e5 + 5;
const int LEN = (1 << 20);
int n, s, L, R;
int f[maxn], t[maxn], c[maxn], q[maxn];
#define nec getchar
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1; ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
inline int getup(int j, int k) { return (f[j] - s * c[j]) - (f[k] - s * c[k]); }
inline int getdown(int j, int k) { return (c[j]) - (c[k]); }
inline int getDP(int i, int j) { return f[j] + (c[i] - c[j]) * t[i] + s * (c[n] - c[j]); }
signed main() {
	read(n); read(s);
	L = R = 1;
	for (int i = 1; i <= n; ++i) {
		read(t[i]); read(c[i]);
		t[i] += t[i - 1];
		c[i] += c[i - 1];
	}
	for (int i = 1; i <= n; ++i) {
		while (L < R && getup(q[L + 1], q[L]) <= t[i] * getdown(q[L + 1], q[L]))
			++L;
		f[i] = getDP(i, q[L]);
		while (L < R && getup(i, q[R]) * getdown(q[R], q[R - 1]) <= getdown(i, q[R]) * getup(q[R], q[R - 1]))
			--R;
		q[++R] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

任务安排 3

双倍经验 | 三倍经验

注意 \(t\) 有可能是负数。

Solution

\(t\) (输入)有可能为负,代表着 \(t\)(前缀和)不再单调递增,用不单调的对象作为单调队列的条件一看就十分不讲武德,这样维护出来的队头显然不是正确答案。

后面的 \(t,c\) 默认为前缀和。

我们感性证明一下。

一个最普通的例子,假设输入了一个负数,导致 \(t_i<t_{i-1}\),且有一斜率 \(>t_i\)\(<t_{i-1}\)

那么在 \(i-1\) 时,它就被维护队头的操作剔掉了,但也许它凑巧就是 \(i\) 的最优解,呜呼哀哉。

于是我们不能删队头元素了。那怎么查询最优解呢?

单调队列里装的始终还是个具有单调性的下凸包,于是我们可以在队列中二分一个 \(pos\)\(pos\)\(pos-1\) 形成的斜率比 \(t_i\) 小,\(pos+1\)\(pos\) 形成的斜率比 \(t_i\) 大。

然后把 \(pos\) 当成 \(q_l\) 来处理就好了。

队尾还是维护一个下凸。

时间复杂度 \(\mathcal O(n\log_2n)\)

Code

强烈建议不要去动 AcWing 版本的那道。

最后两组数据堪称毒瘤。

long long 乘法,卡 __int128 时限,卡 double 精度,非 T 即 WA。

反正我搞了半个小时硬是没有搞出来。

#include <cstdio>
#define int long long
const int maxn = 3e5 + 5;
const int LEN = (1 << 20);
int n, s, L, R;
int f[maxn], t[maxn], c[maxn], q[maxn];
#define nec getchar
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1; ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
inline int getup(int j, int k) { return (f[j] - s * c[j]) - (f[k] - s * c[k]); }
inline int getdown(int j, int k) { return (c[j]) - (c[k]); }
inline int getDP(int i, int j) { return f[j] + t[i] * (c[i] - c[j]) + s * (c[n] - c[j]); }
inline int Func(int u) {
	if (L == R) return q[L];
	int l = L, r = R;
	while (l < r) {
		int mid = l + r >> 1;
		if (getup(q[mid + 1], q[mid]) <= u * getdown(q[mid + 1], q[mid]))
			l = mid + 1;
		else r = mid;
	}
	return l;
}
signed main() {
	read(n); read(s);
	L = R = 1;
	for (int i = 1; i <= n; ++i) {
		read(t[i]); read(c[i]);
		t[i] += t[i - 1];
		c[i] += c[i - 1];
	}
	for (int i = 1; i <= n; ++i) {
		int Best = Func(t[i]);
		f[i] = getDP(i, q[Best]);
		while (L < R && getup(i, q[R]) * getdown(q[R], q[R - 1]) <= getdown(i, q[R]) * getup(q[R], q[R - 1]))
			--R;
		q[++R] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

土地购买

Solution

首先我们想明白一件事情:如果一块土地,有另一块土地的长和宽都比它大,那就不用再理它了,直接从总序列里剔除。

struct _ {
    int w, l;
    bool operator< (const _ q) const {
        return w == q.w ? l > q.l : w > q.w;
    }
} a[maxn];
...

std::sort(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i) {
    if (a[i].l > a[cnt].l)
        a[++cnt] = a[i];
}

然后就推式子。

贪心地想,在前面的操作后,\(a\) 是一个 \(w\) 递减,\(l\) 递增的土地序列。

所以我们选择将连续的一段区间分为一组,这样的话,一个组里的一段连续的土地 \([x,y]\) 就只有 \(w_x\)\(l_y\) 起了作用,又没有中间那一段的事了。

\(f_i\) 表示分配完第 \(i\) 块土地后的最小花费。

则有:

\[f_i=\min\{f_j+w_{j+1}\times l_i\} \]

\(j\) 优于 \(k\)

则有:

\[f_j+w_{j+1}\times l_i<f_k+w_{k+1}\times l_i \\ f_j-f_k<(-w_{j+1}+w_{k+1})\times l_i \\ \dfrac{(f_j)-(f_k)}{(-w_{j+1})+(w_{k+1})}<l_i \]

Code

#include <cstdio>
#include <algorithm>
#define int long long
const int maxn = 5e4 + 5;
struct _ {
    int w, l;
    bool operator< (const _ q) const{
        return w == q.w ? l > q.l : w > q.w;
    }
};
_ a[maxn];
int n, cnt, l, r;
int q[maxn], f[maxn];
int getup(int j, int k) { return f[j] - f[k]; }
int getdown(int j,int k) { return a[k + 1].w - a[j + 1].w; }
int getDP(int i,int j) { return f[j] + a[j + 1].w * a[i].l; }
signed main() {
    scanf("%lld", &n);
    for(int i = 1; i <= n; ++i)
        scanf("%lld %lld", &a[i].w, &a[i].l);
    std::sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; ++i) {
        if (a[i].l > a[cnt].l)
            a[++cnt] = a[i];
    }
    n = cnt; l = r = 1;
    for (int i = 1; i <= n; ++i) {
		while (l < r && getup(q[l + 1], q[l]) <= a[i].l * getdown(q[l + 1], q[l]))
			l++;
		f[i] = getDP(i, q[l]);
		while (l < r && getup(i, q[r]) * getdown(q[r], q[r - 1]) <= getup(q[r],q[r - 1]) * getdown(i, q[r]))
			r--;
		q[++r] = i;
    }
    printf("%lld", f[n]);
    return 0;
}

仓库建设

双倍经验 | 三倍经验

Solution

\(f_i\) 表示在 \(i\) 工厂建立仓库的最小花费。

则有:

\[\begin{aligned} f_i&=\min\{f_j+\sum\limits_{k=j+1}^{i-1}[(x_i-x_k)\times p_k]+c_i\} \\ &=\min\{f_j+\sum\limits_{k=j+1}^{i-1}(x_i\times p_k)-\sum\limits_{k=j+1}^{i-1}(x_k\times p_k)\}+c_i \\ &=\min\{f_j+x_i\times\sum\limits_{k=j+1}^{i-1}p_k-\sum\limits_{k=j+1}^{i-1}(x_k\times p_k)\}+c_i \end{aligned} \]

利用前缀和优化:设 \(a\)\(p\) 的前缀和数组,\(b\)\(x_i\times p_i\) 的前缀和数组。

则原式可化为:

\[\begin{aligned} f_i&=\min\{f_j+x_i\times(a_{i-1}-a_j)-(b_{i-1}-b_j)\}+c_i \\ &=\min\{f_j+x_i\times a_{i-1}-x_i\times a_j-b_{i-1}+b_j\}+c_i \\ &=\min\{f_j-x_i\times a_j+b_j\}+x_i\times a_{i-1}-b_{i-1}+c_i \end{aligned} \]

\(j\) 优于 \(k\)

则有:

\[f_j-x_i\times a_j+b_j<f_k-x_i\times a_k+b_k \\ f_j-f_k+b_j-b_k<x_i\times a_j-x_i\times a_k \\ \dfrac{(f_j+b_j)-(f_k+b_k)}{a_j-a_k}<x_i \]

Code

想感受人生的同学们可以尝试做一下 AcWing 那个版本,非 MLE 即 WA,爽到炸。

#include <cstdio>
#define int long long
const int maxn = 1e6 + 5;
const int LEN = (1 << 20);
int n, L, R;
int f[maxn], x[maxn], p[maxn];
int a[maxn], b[maxn], c[maxn], q[maxn];
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
inline int getup(int j, int k) { return (f[j] + b[j]) - (f[k] + b[k]); }
inline int getdown(int j, int k) { return (a[j]) - (a[k]); }
inline int getDP(int i, int j) { return f[j] - x[i] * a[j] + b[j] + x[i] * a[i - 1] - b[i - 1] + c[i]; }
signed main() {
	read(n); L = R = 1;
	for (int i = 1; i <= n; ++i) {
		read(x[i]); read(p[i]); read(c[i]);
		a[i] = a[i - 1] + p[i];
		b[i] = b[i - 1] + x[i] * p[i];
	}
	for (int i = 1; i <= n; ++i) {
		while (L < R && getup(q[L + 1], q[L]) <= x[i] * getdown(q[L + 1], q[L]))
			++L;
		f[i] = getDP(i, q[L]);
		while (L < R && getup(i, q[R]) * getdown(q[R], q[R - 1]) <= getdown(i, q[R]) * getup(q[R], q[R - 1]))
			--R;
		q[++R] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

锯木厂选址

双倍经验 | 三倍经验

Solution

和仓库建设很像。

\(A_i=\sum\limits_{j=i}^nd_j\),表示第 \(i\) 棵树与山脚的距离;\(B_i=\sum\limits_{j=1}^iw_j\),表示 \(w\) 的前缀和;\(S=\sum\limits_{i=1}^nA_i\times w_i\),表示将所有树全部运到山脚的花费总和。

假设在 \(j\) 处建立第 \(1\) 座锯木厂,在 \(i\) 处建立第 \(2\) 座锯木厂,此时:

  • 对于第 \(1\sim j\) 棵树来说,\(S\) 比实际情况多运了 \(A_j\) 的路程;
  • 对于第 \(j+1\sim i\) 棵树来说,\(S\) 比实际情况多运了 \(A_i\) 的路程。

将对应的多运的距离与花费相乘的结果,让 \(S\) 将其减去即可。

\[\begin{aligned} ans&=\min\{S-A_j\times B_j-(B_i-B_j)\times A_i\}\\ &=\min\{A_i\times B_j-A_j\times B_j\}+S-A_i\times B_i \end{aligned} \]

\(j<k\)\(j\) 优于 \(k\),则有:

\[\begin{aligned} A_i\times B_j-A_j\times B_j&<A_i\times B_k-A_k\times B_k\\ -A_j\times B_j+A_k\times B_k&<-A_i\times B_j+A_i\times B_k\\ \dfrac{-A_j\times B_j+A_k\times B_k}{-B_j+B_k}&<A_i\\ \dfrac{A_j\times B_j-A_k\times B_k}{B_j-B_k}&<A_i\\ \end{aligned} \]

其中,\(A_i\) 具有单调性,可以直接套板子。

Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
const int inf = 1e18;
const int maxn = 1e6 + 5;
const int LEN = (1 << 20);
int w[maxn], d[maxn];
int n, L, R, s, ans = inf;
int q[maxn], a[maxn], b[maxn];
inline int min(int x, int y) { return x < y ? x : y; }
inline int getDP(int i, int j) {
	return s - a[j] * b[j] - (b[i] - b[j]) * a[i];
}
inline int getup(int j, int k) {
	return a[j] * b[j] - a[k] * b[k];
}
inline int getdown(int j, int k) {
	return b[j] - b[k];
}
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
signed main() {
	read(n);
	L = R = 1;
    for (int i = 1; i <= n; ++i) {
		read(w[i]), read(d[i]);
		b[i] = b[i - 1] + w[i];
	}
	for (int i = n; i; --i) {
		a[i] = a[i + 1] + d[i];
		s += a[i] * w[i];
	}
	for (int i = 1; i <= n; ++i) {
		while (L < R && getup(q[L + 1], q[L]) >= a[i] * getdown(q[L + 1], q[L]))
			++L;
		ans = min(ans, getDP(i, q[L]));
		while (L < R && getup(i, q[R]) * getdown(q[R], q[R - 1]) >= getup(q[R], q[R - 1]) * getdown(i, q[R]))
			--R;
		q[++R] = i;
	}
	printf("%lld", ans);
	return 0;
}

Cats Transport

双倍经验 | 三倍经验

经验:洛谷的题面比蓝书和 AcWing 上的标准多了,蓝书的题意不清。

Solution

研究表明,边猛灌养乐多边看蓝书有助于理解。

这道题难在推式子。不然还能难在哪里

\(A_i\) 表示要接到第 \(i\) 只猫的最早出发时间,也就是说,在此时出发,猫 \(i\) 的等待时间为 \(0\)

\(A_i=T_i-\sum\limits_{j=1}^{H_i}D_j\),也就是出发时间=到达时间-经过时间。

此时我们可以把其他所有因素去掉,题意转换为:

已知在 \(\geqslant A_i\) 的时刻出发可以接到猫 \(i\) ,在 \(P\) 次出发次数的限制内接到所有猫,猫的等待时间之和最小是多少?

假设饲养员在 \(t\) 时刻出发,猫 \(i\) 的等待时间就是 \(t-A_i\)

\(A_i\) 从小到大排序。

显然,一次接一段连续的猫,花费自然是最小的。

若我们要接 \([k+1,j]\) 范围内的猫,它们的等待时间之和就是:

\[\begin{aligned} \sum\limits_{p=k+1}^{j}(A_j-A_p)&=[j-(k+1)+1]A_j-\sum\limits_{p=k+1}^{j}A_p \\ &=(j-k)\times A_j-(S_j-S_k) \\ &=j\times A_j-k\times A_j-S_j+S_k \end{aligned} \]

其中 \(S\)\(A\) 的前缀和。

\(f_{i,j}\) 表示前 \(i\) 个饲养员带走前 \(j\) 只猫的最小花费。

则有:

\[\begin{aligned} f_{i,j}&=\min\{f_{i-1,k}+j\times A_j-k\times A_j-S_j+S_k\} \\ &=\min\{f_{i-1,k}-A_j\times k+S_k\}+A_j\times j-S_j \end{aligned} \]

我们将循环地枚举每个饲养员的循环变量 \(i\) 看做常量。

\(x\) 优于 \(y\),则有:

\[f_{i-1,x}-A_j\times x+S_x<f_{i-1,y}-A_j\times y+S_y \\ \dfrac{(f_{i-1,x}+S_x)-(f_{i-1,y+S_y})}{x-y}<A_j \]

注意事项

\(f\) 初始化为极大值。

Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
const int maxp = 105;
const int maxn = 1e5 + 5;
const int LEN = (1 << 20);
int f[maxp][maxn];
int n, m, p, L, R, t, h;
int q[maxn], a[maxn], d[maxn], s[maxn];
inline int getDP(int i, int j, int k) {
	return f[i - 1][k] + (j - k) * a[j] - (s[j] - s[k]);
}
inline int getup(int i, int j, int k) {
	return (f[i - 1][j] + s[j]) - (f[i - 1][k] + s[k]);
}
inline int getdown(int j, int k) {
	return (j) - (k);
}
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
signed main() {
	read(n); read(m); read(p);
	memset(f, 0x3f, sizeof (f));
	for (int i = 2; i <= n; ++i)
		read(d[i]), d[i] += d[i - 1];
	for (int i = 1; i <= m; ++i) {
		read(h); read(t);
		a[i] = t - d[h];
	}
	std::sort(a + 1, a + m + 1);
	for (int i = 1; i <= m; ++i)
		s[i] = s[i - 1] + a[i];
	f[0][0] = 0;
	for (int i = 1; i <= p; ++i) {
		L = R = 1;
		for (int j = 1; j <= m; ++j) {
			while (L < R && getup(i, q[L + 1], q[L]) <= a[j] * getdown(q[L + 1], q[L]))
				++L;
			f[i][j] = getDP(i, j, q[L]);
			while (L < R && getup(i, j, q[R]) * getdown(q[R], q[R - 1]) <= getup(i, q[R], q[R - 1]) * getdown(j, q[R]))
				--R;
			q[++R] = j;
		}
	}
	printf("%lld", f[p][m]);
	return 0;
}

特别行动队

双倍经验 | 三倍经验

Solution

直接推式子。

\[\begin{aligned} f_i&=\max\{f_j+a\times(s_i-s_j)^2+b\times(s_i-s_j)+c\ \}\\ &=\max\{f_j+a\times({s_i}^2-2\times s_i\times s_j+{s_j}^2)+b\times s_i-b\times s_j\ \}+c\\ &=\max\{f_j+a\times{s_i}^2-2\times a\times s_i\times s_j+a\times{s_j}^2-b\times s_j\ \}+b\times s_i+c\\ &=\max\{f_j-2\times a\times s_i\times s_j+a\times{s_j}^2-b\times s_j\ \}+a\times{s_i}^2+b\times s_i+c\\ \end{aligned} \]

\(j\) 优于 \(k\),则有:

\[\begin{aligned} f_j-2\times a\times s_i\times s_j+a\times{s_j}^2-b\times s_j&>f_k-2\times a\times s_i\times s_k+a\times{s_k}^2-b\times s_k\\ (f_j+a\times{s_j}^2-b\times s_j)-(f_k+a\times{s_k}^2-b\times s_k)&>(2\times a\times s_i\times s_j)-(2\times a\times s_i\times s_k)\\ \dfrac{(f_j+a\times{s_j}^2-b\times s_j)-(f_k+a\times{s_k}^2-b\times s_k)}{(2\times a\times s_j) - (2\times a\times s_k)}&>s_i\\ \end{aligned} \]

我们发现中间的符号是 \(>\)

所以我们在弹队头的时候,要把判断条件中的符号反过来。

Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
const int maxn = 1e6 + 5;
const int LEN = (1 << 20);
int n, L, R, a, b ,c;
int q[maxn], s[maxn], f[maxn];
inline int getDP(int i, int j) {
	return f[j] + a * (s[i] - s[j]) * (s[i] - s[j]) + b * (s[i] - s[j]) + c;
}
inline int getup(int j, int k) {
	return (f[j] + a * s[j] * s[j] - b * s[j]) - (f[k] + a * s[k] * s[k] - b * s[k]);
}
inline int getdown(int j, int k) {
	return (2 * a * s[j]) - (2 * a * s[k]);
}
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
signed main() {
	read(n); read(a); read(b); read(c);
	L = R = 1;
	for (int i = 1; i <= n; ++i) {
		read(s[i]), s[i] += s[i - 1];
		while (L < R && getup(q[L + 1], q[L]) >= s[i] * getdown(q[L + 1], q[L]))
			++L;
		f[i] = getDP(i, q[L]);
		while (L < R && getup(i, q[R]) * getdown(q[R], q[R - 1]) <= getup(q[R], q[R - 1]) * getdown(i, q[R]))
			--R;
		q[++R] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

征途

双倍经验 | 三倍经验

Solution

明显,不化掉 \(v\times m^2\) 就无法进行优化。

\(x\) 为当前休息站点与上一休息站点的距离, \(x_0\)\(x_1\sim x_n\) 的平均数,\(S\)\(x\) 的前缀和。

\[\begin{aligned} V\times m^2&=[(x_1-x_0)^2+(x_2-x_0)^2+\cdots+(x_m-x_0)^2]\times m \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2+(m\times {x_0})^2-2\times (x_0\times m)\times \sum\limits_{i=1}^{m}x_i \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2+{S_m}^2-2\times{S_m}^2 \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2-{S_m}^2 \end{aligned} \]

其中 \(S_m\) 是一个定值(即输入所有路的长度和)。

唯一需要计算的,就是 \(\sum\limits_{i=1}^{m}{x_i}^{2}\),所以我们就来 DP 它。

\(a\) 为输入道路长度的前缀和数组,\(f_{i,j}\) 表示第 \(i\) 次休息在 \(j\) 处时 \(\min\{\sum\limits_{k=1}^{i}{x_k}^2\}\) 的值,则有:

\[\begin{aligned} f_{i,j}&=\min\{f_{i-1,j}+(a_i-a_j)^2\} \\ &=\min\{f_{i-1,j}+{a_i}^2-2\times a_i\times a_j+{a_j}^2\} \end{aligned} \]

\(j\) 优于 \(k\),则有:

\[f_{i-1,j}-2\times a_i\times a_j+{a_j}^2<f_{i-1,k}-2\times a_i\times a_k+{a_k}^2 \\ f_{i-1,j}-f_{i-1,k}+{a_j}^2-{a_k}^2<2\times a_i\times(a_j-a_k) \\ \dfrac{f_{i-1,j}-f_{i-1,k}+{a_j}^2-{a_k}^2}{2\times(a_j-a_k)}<a_i \]

注意事项

初始化。

\(f\) 初始化为极大值。

Code

在 AcWing 上将 memset 改为初始化 f[0][i] = a[i] * a[i] 就可以过了,不然会 MLE,也许是因为没开滚动。(本来也用不着滚

#include <cstdio>
#define int long long
const int inf = 1e18;
const int maxn = 3005;
const int LEN = (1 << 20);
int f[maxn][maxn];
int a[maxn], q[maxn];
int n, m, L, R, ans = inf;
#ifdef ONLINE_JUDGE
inline int nec(void) {
	static char buf[LEN], *p = buf, *e = buf;
	if (p == e) {
		e = buf + fread(buf, 1, LEN, stdin);
		if (e == buf) return EOF; p = buf;
	}
	return *p++;
}
#else
#define nec getchar
#endif
inline bool read(int &x) {
	char ch = nec();
	bool f = 0; x = 0;
	while (ch < '0' || ch > '9') {
		if (ch == EOF) return 0;
		if (ch == '-') f = 1;
		ch = nec();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + ch - '0';
		ch = nec();
	}
	if (f) x = -x;
	return 1;
}
inline int min(int x, int y) { return x < y ? x : y; }
int getDP(int i, int j, int k) {
	return f[k - 1][j] + (a[i] - a[j]) * (a[i] - a[j]);
}
int getup(int j, int k, int i) {
	return f[i - 1][j] - f[i - 1][k] + a[j] * a[j] - a[k] * a[k];
}
int getdown(int j, int k) {
	return (a[j] - a[k]) << 1;
}
signed main(){
	read(n); read(m);
	for (int i = 1; i <= n; ++i) {
		read(a[i]);
		a[i] += a[i - 1];
		f[0][i] = a[i] * a[i];
	}
	for (int i = 1; i < m; ++i) {
		L = R = 1; q[L] = i;
		for (int j = i; j <= n; ++j) {
			while (L < R && getup(q[L + 1], q[L], i) <= a[j] * getdown(q[L + 1], q[L]))
				++L;
			f[i][j] = getDP(j, q[L], i);
			while (L < R && getup(i, q[R], i) * getdown(q[R], q[R - 1]) <= getup(q[R], q[R - 1], i) * getdown(i, q[R]))
				--R;
			q[++R] = j;
		}
	}
	printf("%lld",m * f[m - 1][n] - a[n] * a[n]);
	return 0;
}

柠檬

双倍经验

Solution

是边写这篇题解边做的(

题意:

有一个含有 \(n\) 个元素的序列 \(s\)。将这个序列分成连续的若干段,定义每一段的价值为「在这一段当中任选某个元素的个数的平方再乘上这个元素」的最大值。求将 \(s\) 划分后的最大价值。

不难想到分成的每一段首尾元素必须相等。

比如有这样一个序列 \(x,a_1,a_2,a_3,\cdots,a_k,x,y\) 。假设我们求的是这一段包含 \(x\) 的个数的平方,那么完全可以将 \(y\) 从这一段中分离,单独为一段,明显更优。

而其他所有情况都是这种情况的拓展。

于是得到式子:

\[f_i=\max\{f_{j-1}+s_i\times(cnt_i-cnt_j+1)^2\} \]

其中 \(cnt_i\)\(s_i\) 已经出现了多少次。

然后就是套路。

\[\begin{aligned} f_i&=\max\{f_{j-1}+s_i\times({cnt_i}^2+{cnt_j}^2-2\times cnt_i\times cnt_j+2\times cnt_i-2\times cnt_j+1)\} \\ &=\max\{f_{j-1}+s_i\times{cnt_i}^2+s_i\times{cnt_j}^2-2\times s_i\times cnt_i\times cnt_j+2\times s_i\times cnt_i-2\times s_i\times cnt_j+s_i\} \\ &=\max\{f_{j-1}+s_i\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_j\}+s_i\times {cnt_i}^2+2\times s_i\times cnt_i+s_i \end{aligned} \]

这次不是 \(\min\),而是 \(\max\),我们还能像以前那样推吗?

不急,我们先按以前的方法试试,见机行事。

\(j\) 优于 \(k\),则有:

\[f_{j-1}+s_i\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_j>f_{k-1}+s_i\times {cnt_k}^2-2\times s_i\times cnt_i\times cnt_k-2\times s_i\times cnt_k \]

暴力搞不出来,因为那个 \(s_i\) 乘上的平方项。

那我们想一个办法,把 \(s_i\) 给灭掉就行了。

既然我们已知一段的首尾元素必须相等,那不就说明 \(s_i,s_j,s_k\) 可以相互替换吗?

继续搞。

\[f_{j-1}+s_j\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_j\times cnt_j>f_{k-1}+s_k\times {cnt_k}^2-2\times s_i\times cnt_i\times cnt_k-2\times s_k\times cnt_k \\ f_{j-1}-f_{k-1}+s_j\times {cnt_j}^2-s_k\times {cnt_k}^2-2\times s_j\times cnt_j+2\times s_k\times cnt_k>2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_i\times cnt_k \\ \dfrac{f_{j-1}-f_{k-1}+s_j\times {cnt_j}^2-s_k\times {cnt_k}^2-2\times s_j\times cnt_j+2\times s_k\times cnt_k}{cnt_j-cnt_k}>2\times s_i\times cnt_i \]

这里的符号变成 \(>\) 了。

易得,这里要维护的是一个斜率单调递减的上凸包最优解序列。

因为 \(s_i\) 必须与 \(s_j,s_k\) 相同,明显要针对每个 \(s_i\) 维护不同的最优解序列,在每次对应的序列中计算。

维护的斜率单调递减,而对于每一个相等的 \(s_i\)\(2\times s_i\times cnt_i\) 一定是单调递增的,导致了这个序列大概长这个样子:斜率单调递减,但末尾的最小的斜率仍大于 \(2\times s_i\times cnt_i\)

单调栈可以自行研究,因为作者瞄了一眼时间发现已经是明天了。(什

Code

#include<cstdio>
#include<vector>
using namespace std;
#define int long long
#define top q[x].size() - 1
const int maxn = 1e5 + 5;
const int maxm = 1e4 + 5;
int n;
vector<int> q[maxm]; // 不用 vector 会 MLE
int f[maxn], s[maxn];
int cnt[maxn], tot[maxm];
int getDP(int i,int j){
	return f[j-1]+(cnt[i]-cnt[j]+1)*(cnt[i]-cnt[j]+1)*s[i];
}
int getup(int j,int k){
	return f[j-1]-f[k-1]+s[j]*cnt[j]*cnt[j]-s[k]*cnt[k]*cnt[k]-2*s[j]*cnt[j]+2*s[k]*cnt[k];
}
int getdown(int j,int k){
	return cnt[j]-cnt[k];
}
signed main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;++i){
		scanf("%lld",&s[i]);
		cnt[i]=++tot[s[i]];
    }
    for(int i=1;i<=n;++i){
        int x=s[i];
		while(q[x].size()>=2&&getup(q[x][top-1],i)*getdown(q[x][top-1],q[x][top])>=getup(q[x][top-1],q[x][top])*getdown(q[x][top-1],i))
            q[x].pop_back();
		q[x].push_back(i);
		while(q[x].size()>=2&&getDP(i,q[x][top])<=getDP(i,q[x][top-1]))
            q[x].pop_back();
		f[i]=getDP(i,q[x][top]);
	}
	printf("%lld",f[n]);
	return 0;
}

总结

总结斜优的步骤:

  1. 推 DP 式子
  2. 对式子进行巧妙的优化,使其易于化简
  3. 假设 \(j\) 优于 \(k\),将式子化成 \(\dfrac{(j)-(k)}{(j)-(k)}> \text{or} <x\) 的形式
  4. 单调队列或单调栈优化
posted @ 2021-02-14 00:12  XSC062  阅读(84)  评论(0编辑  收藏  举报