斜率优化 DP 笔记

· · 算法·理论

Upd on 2025.11.05:例 3 公式错误。

零、闲话

更坏的阅读体验

本文提供一套斜率优化 DP 较为通用的解题方法,由于大部分题目技巧共通,所以限于篇幅不会作太深入的分析,只会对几道经典题详细解释。

今天是 2025.10.31,祝大家 CSP-S2025 RP++。\ 这是四个月前写的东西了,现在才想起来没搬到洛谷。

本文 markdown 的颜色渲染好像有点问题,把大括号也一起染色了,不过不影响阅读。

之前自学的时候总是找不到一个合适的理解方法。\ 还会遇到各种奇怪的问题:例如“什么类型的式子可以斜率优化?”“什么情况要用什么类型的优化?”……等等。

另外网上的笔记要么不详细,要么冗余的部分很多,例如:

是时候解决这些问题了。

一、引入

这个引入可能比较奇怪……但它确实很好地描述了斜率优化的本质。

你需要维护一个点集 (x_i,y_i),每次给定 kq 次查询 k x_i + y_i 的最大值。

上述问题等价于给定 (a,b),求 a x_i + b y_i 的最大值,当只给 k 相当于是查询 \frac{a}{b} x_i + y_i 的最大值(如果 b \lt 0 就怎么取反一下求最小值即可)。

然而,这个点一定在凸包上,否则考虑反证,一定存在更远的点。

凸包相关-计算几何初步

于是现在只考虑简化版的问题:给定 kk x_i + y_i 的最大(小)值。\ 这样转化模型就可以解释斜率优化中一个很重要的点:\max 决策维护上凸壳,\min 决策维护下凸壳。 原因显然。

\max 为例,如何在上凸壳找到最优解?\ 并不难发现,“远近”一定是单峰的。\ 考虑把“远近”刻画成在 (0,0)-(k,1) 直线上的投影,由于凸包的凸性,距离显然是单峰,有单调性。\ 单调性!二分! 不对,三分!

其实二分和三分都可以做。\ 三分显然可以解决,因为函数单峰。\ 至于二分,每次选中一个点 mid,判定它和它后一个点之间的斜率是上升还是下降的即可,对于 \max 决策,只需要求最左边的点使得它和它下一个点斜率下降,这是好求的。\ 至此,这个问题可以在 O(q \log n) 的复杂度内解决。

还有更优的复杂度!\ 考虑把 k 离线下来排序,这样保证按顺序处理的时候 k 单调。\

---------- 上面大概做的事情就是**维护一个点集(凸壳),支持根据参数查找函数最值**。\ 函数**最值**这件事情启发我们将其运用在**动态规划决策单调性**方面的优化。 # 二、斜率优化 ~~老师说不要学这种推法,没有几何直观。~~\ 但至少可以总结出来一种比较模板化的解题方法。\ 而且试了很多次,还挺顺的。 ## 大致流程 (假设 $i$ 从 $j$ 转移而来) 1. 分离和 $j$ 无关的参数 $c$。 2. 提取只和 $j$ 有关的参数 $y$。 3. 确定 $k, x, y, c$。 4. 推式子。 5. 确定维护方法。 ## 注意事项 1. 推式子的时候,如果移项有**负数**,要变号。 2. $\color{red}{需要根据实际的式子确定怎样维护上(下)凸壳}$。 - 若 $k$ 单调递减 $x$ 单调递增,直接用**单调队列**维护,取队头转移 $O(n)$。 - 若 $k$ 单调递增 $x$ 单调递增,需要用**单调栈**维护,取栈顶转移 $O(n)$。 - 若只有 $x$ 单调递增,依然采用**单调队列**,但需要**二分查找**最优决策 $O(n \log n)$。 - 找到一个点使得左边斜率和右边斜率分别大于或小于某个值。 - 若 $k, x$ 均不单调,可以用 **CDQ 分治思想或李超线段树**维护,好像也可以用**平衡树**吧 $O(n \log n)$。 3. 观察初始式子是否能化为“只和 $i$ 有关项”“只和 $j$ 有关项”“和 $i, j$ 都有关项”,再尝试斜率优化。在推式子之前应当先尝试是否有更简单或更显然的优化方法。 ---------- # 三、例题 题目**大致**按个人认为的难度排序。 ## 例 1:[AT_dp_z Frog 3](https://www.luogu.com.cn/problem/AT_dp_z) 这是我写的第一道斜率优化,其它一般都是该题的变种或加上各种参数。\ 以这道经典题把斜率优化的流程大致过一遍。\ 个人认为这篇题解写得很好:[学习链接](https://www.luogu.com.cn/article/ta88qdqn)。\ 显然有: $$dp_i = \min\limits_{j=0}^{i-1} \{ dp_j + (h_i - h_j)^2 + C \}$$ ### 第一步:分离和 $j$ 无关的参数。 $$dp_i = \min\limits_{j=0}^{i-1} \{ dp_j - 2h_ih_j + h_j^2 \} + C + h_i^2$$ ### 第二步:提取只和 $j$ 有关的参数。 设 $y(j) = dp_j + h_j^2$: $$dp_i = \min\limits_{j=0}^{i-1} \{ y(j) - 2h_ih_j \} + C + h_i^2$$ ### 第三步:确定 $k, x, y, c$。 $$dp_i = \min\limits_{j=0}^{i-1} \{ \color{blue}{-2h_i} \color{red}{h_j} + \color{green}{y(j)} \} + \color{purple}{C + h_i^2}$$ 对于上式,有 $\color{blue}{k=-2h_i}, \color{red}{x=h_j}, \color{green}{y = y(j)}, \color{purple}{c = (C + h_i^2)}$。\ 相当于是求 $kx+y$ 的**最小值**,维护**下凸壳**,最后加上常数 $c$。 ### 第四步:推式子 一般是把 $k,x,y$ 带进去形成不等式,移项推出只和 $k$ 相关的不等式: 设 $j_1 \lt j_2$,且 $j_1$ 的决策比 $j_2$ 更优,则有: $$k x_1 + y_1 \leq k x_2 + y_2$$ $$k(x_1 - x_2) \leq y_2 - y_1$$ 将 $x,y$ 带进去: $$k( h_{j_1} - h_{j_2} ) \leq y(j_2) - y(j_1)$$ 移项,注意本题 $h$ 单调递增所以要变号,同时把 $k=-2h_i$ 代入: $$-2h_i \geq \frac{y(j_2) - y(j_1)}{x_1 - x_2}$$ $$2h_i \leq \frac{y(j_1) - y(j_2)}{x_1 - x_2}$$ ### 第五步:确定维护方法 右边长得像一个**斜率**的式子,这意味着我们用 $k=2h_i$ 的直线去“逼近”这个下凸壳,通过之前说的二分方法就可以找到最优决策进行转移。\ 由于要维护下凸壳,并且我们在 DP 要支持**动态加入点**,所以考虑用**单调队列**维护这个过程。\ 具体地,对于队列中的元素,必须保证**两两之间斜率单调上升**。 之前说过可以用二分找到队列中的最优决策点,但此时不需要这么麻烦。\ 由于我们用 $k=2h_i$ 去逼近,而 $h_i$ 又是**单调递增**的,所以**最优决策点单调向后移动**,在单调队列中不断**弹出队头**去掉不优的决策即可。 ```cpp #include <bits/stdc++.h> using namespace std; const int N = 2e5 + 15; int n, h[N]; long long C; int q[N], st, ed; long long dp[N]; double x(int p) { return h[p]; } double y(int p) { return dp[p] + h[p] * 1ll * h[p]; } double slope(int a, int b) { return (y(b) - y(a)) * 1.00 / (x(b) - x(a)); } int main() { scanf("%d%lld", &n, &C); for (int i = 1; i <= n; i++) scanf("%d", &h[i]); st = ed = 1, q[ed] = 1; for (int i = 2; i <= n; i++) { while (st < ed && slope(q[st], q[st + 1]) <= 2 * h[i]) st++; int j = q[st]; dp[i] = dp[j] + (h[i] - h[j]) * 1ll * (h[i] - h[j]) + C; while (st < ed && slope(q[ed - 1], q[ed]) >= slope(q[ed], i)) ed--; q[++ed] = i; } printf("%lld\n", dp[n]); return 0; } ``` ## 例 2:[洛谷 P3195 [HNOI2008] 玩具装箱](https://www.luogu.com.cn/problem/P3195) 并不难写出普通转移方程(设 $s$ 为 $c$ 的前缀和): $$dp_i = \min\limits_{j=0}^{i-1} \{ dp_j + (i-j-1+s_i-s_j-L)^2 \}$$ 考虑暴力展开平方,发现有整整 $36$ 项,爽飞了。\ 推式子需要一些手法……考虑我们斜率优化之前要做什么?\ **分离 $i, j$ 相关的项。**\ 所以设 $a_i = s_i + i, b_j = s_j + j + L + 1$,所以式子就简化为: $$dp_i = \min\limits_{j=0}^{i-1} \{ dp_j + (a_i - b_j)^2 \}$$ $$dp_i = \min\limits_{j=0}^{i-1} \{ -2a_ib_j + dp_j + b_j^2 \} + a_i^2$$ 一样地,设 $j_1 \lt j_2$,且 $j_1$ 比 $j_2$ 优……然后和上一题类似,最后的式子是: $$2 a_i \leq \frac{y_1 - y_2}{x_1 - x_2}$$ $a, b$ 都单增,所以 $k, x$ 都单增,因此直接单调队列维护即可。 ## 例 3:[洛谷 P5785 [SDOI2012] 任务安排](https://www.luogu.com.cn/problem/P5785) 一道很经典的斜率优化题,也是蓝书上的斜优入门题,就详细记录一下吧。 朴素 DP: 由于我们需要知道 $S$ 被计算了几次,所以相应的也需要知道这是第几批次的任务。\ 设 $dp_{i,j}$ 表示分了 $i$ 批次完成前 $j$ 个任务的最小代价。\ 记 $st,sc$ 分别为 $t,c$ 的前缀和。\ 则有转移式: $$dp_{i,j}=\min\limits_{0 \leq k \lt j} \{ dp_{i-1,k} + (S \times i + st_j) \times (sc_j - sc_k) \}$$ 注意是 $sc_j - sc_k$ 不是 $sc_j - sc_{k-1}$。\ 这样空间是 $O(n^2)$,时间复杂度是 $O(n^3)$。由于卡空间需要滚动数组优化到 $O(n)$。 ---------- 蓝书上介绍了一种**费用提前计算**的思想。\ 设 $dp_{i}$ 表示把前 $i$ 个任务分成若干组的最小代价。\ 发现对于每一组任务,$S$ 的贡献计算是单增的。即 $S,2S,3S,\dots,kS$。\ 根据分配律,考虑把代价的计算中 $st$ 和 $S$ 的部分分开来计算。在当前任务提前计算好后面任务中 $S$ 的贡献。 即: $$dp_{i} = \min\limits_{0 \leq j \lt i} \{ dp_{j} + (sc_i - sc_j) \times st_i + (sc_n - sc_j) \times S \}$$ 此时空间复杂度是 $O(n)$,时间复杂度 $O(n^2)$,可以通过弱化版。 ---------- 现在 $n \leq 3 \times 10^5$,考虑 $O(n)$ 等更优的做法。 $$dp_{i} = \min\limits_{0 \leq j \lt i} \{ dp_{j} + (sc_i - sc_j) \times st_i + (sc_n - sc_j) \times S \}$$ 设求 $dp_i$ 时,$j_1 \lt j_2 \lt i$,并且 $j_1$ 优于 $j_2$。 则有: $$dp_{j_1} + (sc_i - sc_{j_1}) \times st_i + (sc_n - sc_{j_1}) \times S \leq dp_{j_2} + (sc_i - sc_{j_2}) \times st_i + (sc_n - sc_{j_2}) \times S$$ $$dp_{j_1} + sc_i \times st_i - sc_{j_1} \times st_i + sc_n \times S - sc_{j_1} \times S \leq dp_{j_2} + sc_i \times st_i - sc_{j_2} \times st_i + sc_n \times S - sc_{j_2} \times S$$ $$dp_{j_1} - sc_{j_1} \times st_i - sc_{j_1} \times S \leq dp_{j_2} - sc_{j_2} \times st_i - sc_{j_2} \times S$$ $$dp_{j_1} - dp_{j_2} - sc_{j_1} \times S + sc_{j_2} \times S \leq sc_{j_1} \times st_i - sc_{j_2} \times st_i$$ 移项注意负数变号。 $$\frac{dp_{j_1} - dp_{j_2} - (sc_{j_1} - sc_{j_2}) \times S}{sc_{j_1} - sc_{j_2}} \geq st_i$$ $$\frac{dp_{j_1} - dp_{j_2}}{sc_{j_1} - sc_{j_2}} - S \geq st_i$$ 取 $\min$,所以单调队列维护**下凸壳**。\ 由于 $st_i$ 单增,可以直接取队头进行转移。 一定不要把 `slope(q[l-1],q[l])` 写成 `slope(l-1,l)`。\ ~~这是很久以前写的题解了。~~\ 这么推会有点麻烦,事实上可以先找出 $x, y$ 然后**换元**,推完再代入进去会更顺。 ## 例 4:[洛谷 P3628 [APIO2010] 特别行动队](https://www.luogu.com.cn/problem/P3628) 设 $S$ 表示 $x$ 的前缀和。 $$dp_i = \max\limits_{j=0}^{i-1} \{ dp_j + A(s_i-s_j)^2 + B(s_i-s_j) + C \}$$ 看到平方就应该很敏锐地发现是斜优了,因为平方是很明显的 $i,j$ 各部分相关的形式。\ 和之前相同地,一样转化形式: $$dp_i = \max\limits_{j=0}^{i-1} \{ \color{blue}{-2As_i}\color{red}{s_j} + \color{green}{dp_j + As_j^2 - Bs_j} \} + \color{purple}{As_i^2 + Bs_i + C}$$ 推式子: $$2As_i \geq \frac{y_1 - y_2}{x_1 - x_2}$$ 取 $\max$ 决策,上凸壳;$A \lt 0$ 所以 $k$ 单减 $x$ 单增,单调队列队头转移。 于是把 $x, y$ 带进去就做完了。 ## 例 5:[洛谷 P2900 [USACO08MAR] Land Acquisition G](https://www.luogu.com.cn/problem/P2900) 首先发现如果一个矩形被另一个矩形完全包含,则它一定不会对答案有贡献。\ 因此使用**单调栈** $O(n)$ 处理出有用的矩形,画在平面直角坐标系上 应该是个阶梯状的东西。 之后 $O(n^2)$ DP 是简单的,设 $f_i$ 表示前 $i$ 片土地分组的最小代价。 $$f_i = \min\limits_{j=1}^{i-1} dp_{j-1} + x_i \times y_j$$ 所以考虑优化,发现这东西 $i,j$ 相关的项分别相乘,就很斜率。\ 由于式子很简单,而且换元又会有变量名冲突,可以直接对原式推导: 设 $j_1 \lt j_2$ 且 $j_1$ 优于 $j_2$: $$x_i \times y_{j_1} + dp_{j_1} \leq x_i \times y_{j_2} + dp_{j_2}$$ 移项,注意 $y_j$ 单调递减要变号。 $$x_i \geq \frac{dp_{j_1} - dp_{j_2}}{y_{j_1} - y_{j_2}}$$ $k, x$ 都递增,可以直接队头转移。 ## 例 6:[洛谷 P2120 [ZJOI2007] 仓库建设](https://www.luogu.com.cn/problem/P2120) 设 $f_i$ 表示强制建 $i$ 以保护 $[1,i]$ 物品的最小代价。\ 则容易写出状态转移方程,枚举上一次在 $j$ 建立仓库: $$f_i = \min\limits_{j=0}^{i-1} \{ f_j + c_i + (\sum\limits_{k=j+1}^{i-1} (x_i - x_k) \times p_k) \}$$ 把式子拆一下,得到: $$f_i = \min\limits_{j=0}^{i-1} \{ f_j + ( \sum\limits_{k=j+1}^{i-1} p_k ) \times x_i - (\sum\limits_{k=j+1}^{i-1} x_k \times p_k) \} + c_i$$ 设 $sp$ 为 $p$ 前缀和,$s$ 为 $x \times p$ 前缀和。 $$f_i = \min\limits_{j=0}^{i-1} \{ f_j + (sp_{i-1} - sp_j) - (s_{i-1} - s_j) x_i \} + c_i$$ $$f_i = \min\limits_{j=0}^{i-1} \{ \color{green}{f_j - sp_j} + \color{blue}{s_j} \color{red}{x_i} \} + \color{purple}{ c_i + sp_{i-1} - s_{i-1} }$$ 然后设 $j_1 \lt j_2$,但 $j_1$ 优于 $j_2$,移项得到: $$x_i \leq \frac{(f_{j_1} + s_{j_1})-(f_{j_2} + s_{j_2})}{sp_{j_1} - sp_{j_2}}$$ $x, k$ 都单调递增,直接单调队列队头转移。 ## 例 7:[洛谷 P5504 [JSOI2011] 柠檬](https://www.luogu.com.cn/problem/P5504) 假设 $c_i$ 表示 $a_i$ 颜色中 $i$ 是第 $c_i$ 个出现的。\ 则不难写出转移方程: $$dp_i = \max\limits_{j \leq i, a_i = a_j} \{ dp_{j-1} + a_i \times (c_i - c_j + 1)^2 \}$$ 看到平方,果断拆式子: $$dp_i = \max\limits_{j \leq i, a_i = a_j} \{ dp_{j-1} + a_i \times (c_i^2 + c_j^2 + 1 - 2c_ic_j + 2c_i - 2c_j) \}$$ 提取 $k, x, y, c$: $$dp_i = \max\limits_{j \leq i, a_i = a_j} \{ \color{green}{dp_{j-1} + c_j^2a_j - 2c_ja_j} \color{blue}{-2c_ia_i}\color{red}{c_j} \} + \color{purple}{a_i + a_ic_i^2 + 2a_ic_i}$$ 然后再假设 $j_1 \lt j_2$,$j_1$ 优于 $j_2$,得到: $$2a_ic_i \geq \frac{y_1 - y_2}{x_1 - x_2}$$ 由于 $k, x$ 双单增,需要用单调栈维护(卡了很久单调队列)。 ## 例 8:[洛谷 P5017 [NOIP 2018 普及组] 摆渡车](https://www.luogu.com.cn/problem/P5017) 设 $f_i$ 表示运走前 $i$ 个单位时间来的人,最小的等待时间是多少。\ 容易得到转移方程: $$f_i = \min\limits_{j=0}^{i-m} \{ f_j + \sum\limits_{j \lt t_k \leq i} (i - t_k) \}$$ 第一想法是通过 $i,j$ 二分出 $t$ 序列合法的 $l,r$,但显然由于 $t$ 足够小可以直接在上面做**前缀和**。\ 前缀和优化一下,记 $s$ 为 $t$ 的前缀和,$c$ 为 $[0,i]$ 时间内来了多少人。 $$f_i = \min\limits_{j=0}^{i-m} \{ f_j + (c_i - c_j) \times i - (s_i - s_j) \}$$ 复杂度 $O(T^2)$。 然后可以减少无用转移:发现 $\geq 2m$ 的段**分成两部分 $m$ 一定不劣**,所以减小转移范围,复杂度 $O(Tm)$。 还有优化空间!再看一眼式子: $$f_i = \min\limits_{j=\max(0, i-2m+1)}^{i-m} \{ f_j + (c_i - c_j) \times i - (s_i - s_j) \}$$ 乘积形式已经出来了,那么展开后,照常推式子。\ 结果是: $$\frac{(f_{j_1} + s_{j_1}) - (f_{j_2} + s_{j_2})}{c_{j_1} - c_{j_2}} \geq i$$ 这玩意儿的 $k=i$ 是单增的,$x=c_j$ 也是单增的,所以可以直接**单调队列队头**转移。 ## 例 9:[洛谷 P4072 [SDOI2016] 征途](https://www.luogu.com.cn/problem/P4072) 平均数: $$\bar{s} = \frac{\sum\limits_{i=1}^{m} s_i}{m}$$ 方差: $$v = \frac{1}{m} \sum\limits_{i=1}^{m} (s_i - \bar{s})^2$$ $$v = \frac{1}{m} \Big[ \sum\limits_{i=1}^{m} s_i^2 - 2 \bar{s} (\sum\limits_{i=1}^{m} s_i) + m \bar{s} ^2 \Big]$$ 化简得: $$v \times m^2 = m \times (\sum s_i^2) - (\sum s_i)^2$$ 后者是定值,前者系数 $m$ 也是定值,因此**只要最小化 $\sum s_i^2$ 即可**。\ $O(m n^2)$ DP 是显然的,设 $dp_{i,j}$ 表示走了 $i$ 天,走到 $j$ 这条路,最小代价是多少。 下文 $s$ 为**路径的前缀和**。\ 则有 $dp_{i,j} = \min\limits_{0 \leq k \lt j} \{ dp_{i-1,k} + (s_j - s_k)^2 \}$。 ---------- 然后注意到**平方**,这是一个很典的**斜率优化**的式子。\ 拆式子: $$\min\limits_{0 \leq k \lt j} \{ dp_{i-1,k} + (s_j - s_k)^2 \}$$ $$\min\limits_{0 \leq k \lt j} \{ \color{green}{dp_{i-1,k} + s_k^2} \color{blue}{-2s_k} \times \color{red}{s_j} \} + \color{purple}{s_j^2}$$ 还是设 $k_1 \lt k_2$,$k_1$ 优于 $k_2$: $$dp_{i-1,k_1} + s_{k_1}^2 - 2s_{k_1} \times s_j \leq dp_{i-1,k_2} + s_{k_2}^2 - 2s_{k_2} \times s_j$$ $$s_j \geq \frac{y(k_1) - y(k_2)}{x(k_1) - x(k_2)}$$ $$s_j \geq \frac{(dp_{i-1, k_1}+s_{k_1}^2)-(dp_{i-1,k_2}+s_{k_2}^2)}{s_{k_1}-s_{k_2}}$$ 挺巧的,$k,x$ 都是 $s_j$,且是单增的,所以直接**单调队列**维护,取队头转移就完事了。 ## 例 10:[CF311B. Cats Transport](https://www.acwing.com/solution/content/256906/) 首先考虑 $a_i \leftarrow t_i - \sum\limits_{j=2}^i d_{h_i}$,即算出一个铲屎官最早什么时候出发能带走这只猫。\ 然后就和 $h_i$ 无关了,再把 $a$ 排序,每次相当于选取一段区间,代价是区间每个数与区间最大值差的绝对值之和。\ 容易想到二维 DP 状态 $dp_{i,j}$ 表示前 $j$ 个分成 $i$ 组的最小代价。 设 $s_i$ 为 $a$ 的前缀和,优化一下: $$dp_{i,j} = \min\limits_{0 \leq k \lt j} \{ dp_{i-1,k} + a_j \times (j-k) - (s_j - s_k) \}$$ 这样时间复杂度是 $O(p m^2)$ 的。\ 然后看到相乘直接斜率优化。 $$dp_{i,j} = \min\limits_{0 \leq k \lt j} \{ \color{green}{dp_{i-1,k} + s_k} \color{blue}{-a_j} \color{red}{k} \} + \color{purple}{a_j \times j - s_j}$$ 仍然设 $k_1 \lt k_2$,$k_1$ 决策优于 $k_2$,推导得到: $$\frac{(dp_{i-1,k_1} + s_{k_1}) - (dp_{i-1,k_2} + s_{k_2})}{k_1 - k_2} \geq a_j$$ 双单增,直接单调队列维护下凸壳,队首转移。 ## 例 11:[洛谷 P5468 [NOI2019] 回家路线](https://www.luogu.com.cn/problem/P5468) 很好玩的题,自己推出来了。(虽然最后还是翻题解才发现自己哪里错了……) 这个贡献式子是二次的,而且还和 $u, v, p, q$ 至少四个参数相关,很吓人。\ 第一反应是考虑 DP 做这个事情,由于涉及点之间的转移,以及时间先后的问题,所以,设 $dp_{u, i}$ 表示在 $i$ 时刻到达 $u$ 点的最小烦躁值。\ 假设有一条边 $u \to v$,出发时间 $p$ 到达时间 $q$,则不难写出如下方程: $$dp_{v, q} \leftarrow dp_{u, i} + A(p-i)^2 + B(p-i) + C \quad \quad (i \leq p)$$ 其实就是枚举 $i$ 时刻到达 $u$ 点,然后通过该线路转移到 $v$。\ 这个式子有两个问题: 1. 空间和时间都爆了。 2. 在求 $dp_{v,q}$ 的时候需要保证 $dp_{u,i}$ 都已经计算完。 这里我纠结了挺久的,以为只要保证 $\forall i, dp_{u}$ 被计算完成就行了,接着可以怎么动态开点优化一下空间,时间复杂度再说。\ 但事实是,这张图**不一定是 DAG,不能拓扑排序**。 ---------- 那么不妨跳出来重新看问题,我们只需要保证 $\forall i \leq q, dp_{u,i}$ 被计算完。\ 所以显而易见地,可以按照 $q$(到站时刻)将所有线路**排序**,依次插入这些线路。\ 然后新的问题是,DP 是否有后效性?显然不会,就算有环再绕回来,它也只能影响后面的时刻这个点的 DP 信息。 接下来考虑先排序,然后在上述算法的基础上优化时空复杂度。\ 首先不需要真的开两维的 dp 数组,因为对于一个点 $v$ 有用的状态只有 $\forall (u \to v, p, q)$ 中的 $dp_{v, q}$。 因此动态开点(vector)存 DP 值即可。\ 然后是时间复杂度的问题。 ---------- 看到平方贡献,想到拆式子 **斜率优化**。 $$\min \{ dp_{u, i} + A(p^2-2ip+i^2) + B(p-i) + C\}$$ $$\min \{ \color{green}{dp_{u, i}+Ai^2-Bi} \color{blue}{-2Ap} \color{red}{i} \} \color{purple}{+ Ap^2 + Bp + C}$$ 于是得到 $k, x, y, c$,接下来推式子,设 $i_1 \lt i_2$,则有: $$kx_1 + y_1 \leq kx_2 + y_2$$ $$2Ap \leq \frac{y_1 - y_2}{x_1 - x_2}$$ 观察形式:取 $\min$ 维护下凸壳,$x$ 单调递增但 $k$ 不单调,所以用**单调队列二分**维护。\ 然后愉快地 WA 了洛谷 `#9 #19` 两个点。 > 之前的做法是按 $q$ 排序,每次把当前的 $u \to v$ 完成转移之后**直接扔进 $v$ 的单调队列里面**。\ > 问题是,$p$ 的查询**不具有单调性**,所以如果之后有从 $v$ 出发的列车,最优决策**可能会被刚刚扔进去的覆盖掉**。\ > 所以正确做法是:**按照 $p$ 排序**,每次把 $\leq p$ 的所有待加入 DP 值扔进对应点的单调队列,更新完当前节点 $v$ 的 DP 值之后也把它先存着,延后再加入。 由于红温了所以代码整得很丑……~~最初一版的代码其实还挺好看的。~~ ## 例 12:[洛谷 P3994 高速公路](https://www.luogu.com.cn/problem/P3994) 不难想到设 $f_i$ 表示从 $i$ 跳到根节点的最小代价。 $$f_i = \min\limits_{j \in pa_i} \{ dp_j + (dep_i - dep_j) p_i + q_i \}$$ 有 $i,j$ 相关项相乘的形式!**斜率优化**! $$f_i = \min\limits_{j \in pa_i} \{ \color{green}{dp_j} \color{blue}{-p_i} \color{red}{dep_j} \} + \color{purple}{dep_i p_i + q_i}$$ 设 $dep_{j_1} \lt dep_{j_2}$,且 $j_1$ 比 $j_2$ 更优。 $$k(x_1 - x_2) \leq y_2 - y_1$$ 移项,$x$($dep$)单增,负数变号: $$k \leq \frac{y_2 - y_1}{x_1 - x_2}$$ $$p_i \geq \frac{y_1 - y_2}{x_1 - x_2}$$ $k, x$ 都单增,可以**单调队列队头转移**。 但这是一棵树,怎么做斜率优化呢?\ 如果在从根开始 dfs 的过程中直接维护单调队列,难免会有元素覆盖的情况。\ 本质上问题就是:**一棵子树会影响另一棵子树的下凸壳**。 如果暴力弹出加入(还原现场)是 $O(n^2)$ 的复杂度,那么就不弹出加入了。\ **队头不弹出**,直接保留**完整下凸壳**,转移的时候**二分决策点**是 $O(n \log n)$ 的。\ 然后处理加入元素的问题:发现实际上由于下凸壳的斜率具有**单调性**,可以**二分出队尾弹出到哪里**,然后加入一个新元素 $u$ 只会覆盖原来的一个元素,只需要维护这个东西最后再复原即可。 核心思想就是**利用二分快速查找**,以及**利用队列弹出队尾之后每次只加入一个元素**,把覆盖掉的那个元素给记录一下即可。\ 更详细地,就是**队尾指针移动了,但元素还在**。 ## 例 13:[洛谷 P4027 [NOI2007] 货币兑换](https://www.luogu.com.cn/problem/P4027) 发现一个重要性质:**每次买进一定买完,卖出一定卖完。**\ 如果买进的券在未来可以让你赚钱,那你一定是买完最优,而卖出同理。 那么显然可以干出来一个 $O(n^2)$ 的 DP,设 $f_i$ 为前 $i$ 天能获得的最大收益,考虑枚举 $j$ 表示在 $j$ 这一天**买入所有券**,第 $i$ 天再买回来。\ 不妨设 $s_i = \max\limits_{j=1}^{i} f_j$。\ 若第 $j$ 天买了 $x$ 张 $A$ 券和 $y$ 张 $B$ 券,设 $y_j = t, x_j = R_j t$。 $$R_j t \times A_j + t \times B_j = s_j$$ 解得: $$t = \frac{s_j}{R \times A_j + B_j}$$ $$x_j = \frac{R \times s_j}{R \times A_j + B_j} \quad y_j = \frac{s_j}{R \times A_j + B_j}$$ 转移式: $$f_i = \max\limits_{j=1}^{i-1} \{ x_j \times A_i + y_j \times B_i \}$$ 这和 **斜率优化** 本帖一开始引入的问题是几乎相同的。\ 借用类似的思路,我们将 $ax + by$ 转化为 $kx+b$ 的形式(同除 $B_i$): $$f_i = \max\limits_{j=1}^{i-1} \{ B_i ( \frac{A_i}{B_i} x_j + y_j) \}$$ **它两维都不单增**,可以 CDQ 分治或者平衡树维护上凸壳。\ 也可以**李超线段树**维护。 具体地,“反客为主”。\ 对于**一次函数** $y=kx+b$,原本 $\frac{A_i}{B_i}$ 是 $k$,$x_j, y_j$ 分别是 $x,b$。\ 现在反过来,令 $x = \frac{A_i}{B_i}$,对于每一个 $j$ 相当于一条直线 $k=x_j, b=y_j$。\ 相当于维护 $j \in [1, i-1]$ 这些直线在 $x = \frac{A_i}{B_i}$ 处的最大取值,这是李超线段树的模板题。