斜率优化基础

斜率优化

所谓斜率优化,是指最优化问题的 dp 转移式中某些状态优于另一些状态的条件类似于函数的斜率,这种情况下可以通过维护最优的斜率来减少转移次数。

引入:单调队列斜率优化

Atcoder EDU DP Contest Z - Frog 3

\(n\) 个石头,第 \(i\) 个高度为 \(h_i\),满足高度递增。有一只青蛙要从 \(1\) 跳到 \(n\),每次从 \(x\) 跳到 \(y\) 的体力花费为 \((h_x - h_y)^2 + C\),求最小的体力花费。\(n \le 2 \times 10^5\)

\(f_i\) 为当前在 \(i\) 的最小花费,则 \(f_{i} = \min_{j = 1}^{i - 1} \left(f_j + (h_i - h_j)^2 + C\right)\)。直接这样转移的复杂度为 \(O(n^2)\),显然是不能接受的。

对于一个 \(i\),考虑一个点 \(k \gt j\) 如何不劣于 \(j\)

\[f_k + h_i^2 + h_k^2 - 2h_ih_k + C \le f_j + h_i^2 + h_j^2 - 2h_ih_j + C \\ \implies (f_k + h_k^2) - 2h_ih_k \le (f_j + h_j^2) -2h_ih_j \\ \implies \dfrac{H_k - H_j}{h_k - h_j} \le 2h_i, H_x = f_x + h_x^2 \]

容易发现,不等式左侧的形式为 \(\dfrac{y_2 -y_1}{x_2 - x_1}\),也就是两点间连线的斜率,这也是斜率优化的命名依据。

如何根据这个式子进行优化呢?定义 \(j, k\) 间斜率为 \(\text{slope}(j, k) = \dfrac{H_j - H_k}{h_j - h_k}\),容易发现,对于 \(k \gt j, \text{slope}(k, j) \le 2h_i\)\(k\),其转移到 \(i\) 一定不劣于 \(j\),那么从后往前考虑转移时,就不需要考虑 \(j\) 了。

换个角度考虑,我们发现转移式为

\[f_i = \min_{j = 1}^{i - 1} (-2h_jh_i + H_j) + h_i^2 + C \]

\(\min\) 的部分可以看作:有 \(i - 1\) 条直线,第 \(j\) 条为 \(y = -2h_j \times x + H_j\),求这些直线在 \(x = h_i\) 时纵坐标最小的点,也就是直线 \(x = h_i\) 与这 \(i - 1\) 条直线相交的最低点。那么,考虑以下三条直线 \(j, k, l\)

容易发现,用一条竖直直线 \(x = x_0\) 去与这些直线相交求最低点,有一些情况是一条直线不劣于另一条直线的:

  • \(x_0 \le p_1\) 时,\(j\) 不劣于 \(k\),当 \(x_0 \gt p_1\) 时,\(k\) 不劣于 \(j\)
  • \(x \le p_2\) 时,\(k\) 不劣于 \(l\),当 \(x \gt p_2\) 时,\(l\) 不劣于 \(k\)

那么,每条直线都在某一个横坐标区间里不劣于其他所有直线(此时它表现为一条线段),我们将这些线段维护出来,实际上就是在维护这些直线形成的上凸壳

注意到本题 \(h_i\) 是递增的,那么 \(-2h_i\) 是递减的,设当前上凸壳中的直线按顺序为 \(L_{a_1}, L_{a_2}, \dotsb, L_{a_t}\),则新插入一条 \(L : y = -2h_lx + H_l\) 直线,其会从凸壳中删除的 \(L_{a_x}\) 满足 \(L_{a_x}\)\(L\) 的交点在 \(L_{a_x}\)\(L_{a_{x - 1}}\) 左侧(也就是横坐标更小),例如下图中 \(l\) 会从凸壳中删除 \(k\)

写出上述条件的表达式:

\[\dfrac{H_l - H_{a_x}}{-2h_{a_x} - (-2h_l)} \le \dfrac{H_{a_{x}} - H_{a_{x - 1}}}{-2h_{a_{x - 1}} - (-2h_{a_{x}})} \iff \dfrac{H_l - H_{a_x}}{h_l - h_{a_x}} \le \dfrac{H_{a_x} - H_{a_{x - 1}}}{h_{a_x} - h_{a_{x - 1}}} \]

容易发现,这实际上就是 \(\text{slope}(l, a_x) \le \text{slope}(a_x, a_{x - 1})\)。容易发现,\(H_i\) 递增而 \(-2h_i\) 递减,所以每次直线 \(i\) 的最优范围是一段后缀(因为是上凸壳),也就是说会被删除的直线位于凸壳上的一段后缀里;而每次要找的转移直线位于凸壳左侧,所以可以使用单调队列来维护这个凸壳,每次插入直线 \(i\) 时就用上面的条件弹出队尾的直线。由于本题 \(h_i\)(要求交点的横坐标)是递增的,若 \(\dfrac{H_{a_{l + 1}} - H_{a_l}}{2h_{a_{l + 1}} - 2h_{a_{l}}} \lt h_i\),也就是当前队头的直线与凸壳中下一条直线的交点横坐标小于 \(h_i\),那么就可以弹出队头的直线了(因为 \(h_i\) 递增,所以之后也不会用到凸壳上的这一段了)。容易发现,这一条件和我们一开始观察的 \(k\) 不劣于 \(j\) 的条件相同,这也说明了这个方法的正确性。那么本题的代码就很好写出了:

int n, h[N];
ll C, f[N];
inline ll sq(ll x) { return x * x; }
inline ll gx(int i) { return 1ll * h[i]; }
inline ll gy(int i) { return f[i] + sq(h[i]); }
inline ldb slope(int a, int b) {
    return (ldb)(gy(b) - gy(a)) / (gx(b) - gx(a));
}
int q[N], l, r;

int main() {
    rd(n), rd(C);
    q[l = r = 1] = 1;
    for(int i = 1; i <= n; ++i) {
        rd(h[i]);
        if(i == 1) continue;
        while(l < r && slope(q[l], q[l + 1]) < 2 * h[i]) ++l; // 弹出凸壳上的无用直线
        f[i] = f[q[l]] + sq(h[i] - h[q[l]]) + C; // 用队头直线求交点值转移
        while(l < r && slope(q[r - 1], q[r]) >= slope(q[r], i)) --r; // 插入直线 i
        q[++r] = i;
    }
    write(f[n]);
    return 0;
}

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

P3195 [HNOI2008] 玩具装箱

给出长为 \(n\) 的序列 \(\{a_n\}\),你需要将它分为若干段,若 \(a_l \sim a_r\) 分为一段,则代价为 \(\displaystyle (r - l + \sum_{i = l}^{r} a_i - L)^2\)。求最小的分段代价。\(n \le 5 \times 10^4\)

\(f_i\) 表示前 \(i\) 个都划分好段的最小代价,则

\[f_i = \min_{j = 0}^{i - 1} \{f_j + (calc(i, j) - 1 - L)^2\} \]

其中

\[calc(i, j) = i - j + \sum_{k = j + 1}^{i} a_k \]

不妨设 \(pre_i = i + \sum_{j = 1}^{i} a_i\),则 \(calc(i, j) = pre_i - pre_{j}\)。为了方便,我们先把 \(L \to L + 1\),则转移式变为 \(f_i = \min f_j + (pre_i - pre_j - L)^2\)。打开括号可以得到

\[f_i = \min_{j = 0}^{i - 1} \{f_j + (pre_j + L)^2 - 2pre_jpre_i\} + pre^2_i - 2Lpre_i \]

\(H_i = f_i + (pre_i + L)^2\),则

\[f_i = \min_{j = 0}^{i - 1} \{-2pre_j \times pre_i + H_j\} + pre^2_i - 2Lpre_i \]

显然,\(pre_i\)\(i\) 递增,故本题和上一题基本相同,使用单调队列维护所有直线 \(y = -2pre_i \times x + H_i\) 形成的上凸壳即可。

引入\(^2\):单调栈斜率优化

P5504 [JSOI2011] 柠檬

给出长为 \(n\) 的序列 \(\{a_n\}\),你需要把序列分为若干段,为每段 \([l, r]\) 选择一个值 \(x\)(可以不同),则 \([l, r]\) 的价值为 \(x \times cnt^2([l, r], x)\),其中 \(cnt([l, r], x)\) 表示 \(x\) 在区间内的出现次数。求最大价值和。\(n \le 10^5, a_i \le 10^4\)

如果选出的某一段 \([l, r]\) 满足 \(x\) 与左右端点都不相等,则显然可以将左右端点都分离成新的段,这样答案只会增加;而如果与左右端点其中的一个不相等,则不相等的这个也可以分离出去。所以,每个段一定满足 \(a_l = a_r = x\)。设 \(f_i\) 表示划分完了前 \(i\) 个数的答案,则

\[f_i = \max_{j \le i, a_j = a_i} f_{j - 1} + a_i \times cnt^2([j, i], a_i) \]

设前 \(i\) 个数中出现了 \(c_i\)\(a_i\),则

\[\begin{aligned} f_{i} &= \max_{j \le i, a_j = a_i} f_{j - 1} + a_i \times (c_i - c_j + 1)^2 \\ &= \max_{j \le i, a_j = a_i} \{ (f_{j - 1} + a_jc_j^2 - 2a_jc_j) - 2a_ic_ic_j \} + a_ic_i^2 + a_i + 2a_ic_i \end{aligned} \]

\(H_i = f_{i - 1} + a_ic_i^2 - 2a_ic_i, k_i = -2c_j\),则每次求交的竖直直线为 \(x = a_ic_i\)

由于转移是取 \(\max\),所以维护直线形成的下凸壳。对于同一种颜色,\(c_i\) 显然随 \(i\) 递增,故 \(k_i\) 递减。与之前相似地,插入一条直线时,若其与某条直线交点在该条直线与凸壳中上一条直线交点的右侧,则可以从凸壳上删除该条直线。写出表达式:

\[\dfrac{H_i - H_{a_x}}{2c_i - 2c_{a_x}} \ge \dfrac{H_{a_x} - H_{a_{x - 1}}}{2c_{a_x} - 2c_{a_{x - 1}}} \]

\(\text{slope}(i, a_x) \ge \text{slope}(a_x, a_{x - 1})\)。由于 \(a_ic_i\) 递增,若 \(a_x\)\(a_{x - 1}\) 的交点满足横坐标 \(\dfrac{H_{a_x} - H_{a_{x - 1}}}{2c_{a_x} - 2c_{a_{x - 1}}} \lt a_ic_i\),则可以删除直线 \(a_x\),也就是 \(\text{slope}(a_x, a_{x - 1}) \lt 2a_ic_i\)。由于 \(H_i\) 是递增的,而 \(k_i\) 递减,所以直线 \(i\) 的最优范围是一段前缀(因为是下凸壳),而每次要找的交点也在凸壳左侧,所以使用单调栈来维护凸壳。那么对每个颜色分别维护凸壳转移即可。复杂度为 \(O(n)\)

综合上面两个引入可以发现,使用单调队列还是单调栈来维护,取决于新插入的直线与要求的交点是否在凸壳同一侧。但是上面的情况中,要求交点的位置 \(x_i\) 都具有单调性;如果不具有单调性,我们就不知道要求的交点在凸壳的哪个位置。这时应该如何处理?

引入\(^3\):二分最优决策点

P5785 [SDOI2012] 任务安排

\(n\) 个任务,编号为 \(1 \sim n\),每个任务需要花费时间 \(T_i\)。你需要把任务划分为若干段,每段 \([l, r]\) 的耗时为 \(\displaystyle s + \sum_{i = l}^{r} T_i\)。每一段中的所有任务会在同一时刻完成。每个任务的费用是它的完成时刻乘以 \(C_i\)。求最小总费用。\(n \le 3 \times 10^5\)注意,本题 \(T_i\) 可以为负数

\(i\) 个任务的完成时刻至少为 \(t_i = s + \sum_{j = 1}^{i} T_j\),这表示 \(1 \sim i\) 被分为一段;否则,前面每多分出一个段,则 \(t_i\) 增加 \(s\)。不妨在划分 \(i\) 为一段结尾时,将之后对每个任务产生的 \(sC_j\) 这部分贡献放到 \(i\) 处考虑,则设 \(f_i\) 表示完成前 \(i\) 个任务所需的最小费用,\(pT\) 表示 \(T\) 的前缀和,\(pC\) 表示 \(C\) 的前缀和,有

\[\begin{aligned} f_i &= s(pC_n - pC_{i}) + \min_{j = 0}^{i - 1} f_j + pT_i(pC_i - pC_j) \\ &= s(pC_n - pC_{i}) + pT_ipC_i + \min_{j = 0}^{i - 1} (-pC_j \times pT_i + f_j) \end{aligned} \]

\(k_i = -pC_i, H_i = f_i\),显然有 \(H_i\) 递增,\(k_i\) 递减。要求的是最低交点,故维护上凸壳,直线 \(i\) 的最优范围即一段后缀,于是使用队列来维护凸壳。然而,由于神秘的出题人让时间可以为负数,所以 \(pT_i\) 并不随 \(i\) 具有单调性,这时我们只能去上凸壳上二分 \(x_i\) 的位置。具体地,若 \(a_x\)\(a_{x + 1}\) 的交点位于 \(x_i\) 左侧,则显然 \(a_x\) 不是最优的线段;否则,若位于右侧,则 \(a_{x + 1}\) 不是最优的线段。利用这个单调性,在凸壳上二分出最优的线段然后求解交点即可。复杂度为 \(O(n \log n)\)

using ldb = long double;
using ll = long long;
constexpr int MAXN = 300003;
constexpr ldb oo = 1e18;
int n, s;
ll f[MAXN], T[MAXN], C[MAXN];
inline ll gx(int i) { return C[i]; }
inline ll gy(int i) { return f[i]; }
inline ldb slope(int a, int b) {
    ll xa = gx(a), xb = gx(b);
    return (xb ^ xa) ? (ldb)(gy(b) - gy(a)) / (xb - xa) : (gy(b) > gy(a) ? +oo : -oo);
}
int q[MAXN], l, r;
inline int Find(ll lim) {
	if(l == r) return q[l];
	int L = l, R = r + 1, mid;
	while(~(L - R)) {
		mid = L + R >> 1;
		(slope(q[mid - 1], q[mid]) <= lim ? L : R) = mid;
	}
	return q[L];
}

int main() {
    cin >> n >> s;
    for(int i = 1; i <= n; ++i) {
        cin >> T[i] >> C[i];
        T[i] += T[i - 1], C[i] += C[i - 1];
    }
    f[0] = s * C[n];
    for(int i = 1; i <= n; ++i) {
        int j = Find(T[i]);
        f[i] = f[j] + T[i] * (C[i] - C[j]) + s * (C[n] - C[i]);
        while(l < r && slope(q[r - 1], q[r]) >= slope(q[r], i)) --r;
        q[++r] = i;
    }
    cout << f[n];
    return 0;
}

我们解决了 \(x_i\) 不单调的情况,但是如果 \(k_i\) 不单调或 \(H_i\) 不单调,我们就无法得知新插入直线的最优横坐标范围,这时又如何解决呢?

引入\(^4\):李超线段树优化 dp

李超线段树是支持以下操作的数据结构:1. 插入一条直线或线段;2. 查询所有直线或线段中,与 \(x = x_0\) 相交的最高点 / 最低点。对于插入操作全部为直线的情况,可以在 \(O(q \log M)\) 时间内维护,其中 \(M\) 是横坐标值域;对于存在线段的情况,可以在 \(O(q \log^2M)\) 时间内维护(模板题:P4097 【模板】李超线段树 / [HEOI2013] Segment)。

我们首先讨论只插入直线,求相交纵坐标最大值的情况。李超树对每个横坐标区间维护“大多数时候是最优”的直线——在该区间超过一半的横坐标处都最优的直线,并且保证每条直线至多位于一个树节点中。

插入一条直线的过程如下:\(\textbf{Insert}(u, l, r, L)\) 表示在 \(u\) 节点(横坐标区间为 \([l, r]\))考虑插入直线 \(L\)

  • \([l, r] = \empty\):退出;

  • \(u\) 节点没有直线:直接将 \(L\) 设为 \(u\) 节点处的最优直线,退出;

  • \(u\) 节点有直线:设该直线为 \(L_u\),求出 \(L_u\)\(L\) 的交点 \(x\) 和区间中点 \(mid = \dfrac{l + r}{2}\),设 \(L\)\(L_u\) 中斜率更大的一条为 \(L_1\),另一条为 \(L_2\)

    • \(x \le mid\),则说明 \(L_1\)\([mid + 1, r]\) 区间内严格优于 \(L_2\),且在 \([l, mid]\) 区间内可能优于 \(L_2\),此时将 \(L_u\) 设为 \(L_1\),并将 \(L_2\) 向左子树插入:\(\textbf{Insert}(\text{lson}(u), l, mid, L_2)\)。如下图:

    • 否则,\(x \gt mid\),这说明 \(L_2\)\([l, mid]\) 区间内严格优于 \(L_1\),且在 \([mid + 1, r]\) 区间内可能优于 \(L_1\),此时将 \(L_u\) 设为 \(L_2\) 并将 \(L_1\) 插入到右子树:\(\textbf{Insert}(\text{rson}(u), mid + 1, r, L_1)\)

显然,树高为 \(O(\log M)\),且每条直线插入时至多访问 \(O(\log M)\) 个节点。由于每条直线至多位于一个树节点上,所以空间复杂度为 \(O(n)\)

查询操作只需仿照线段树上的查询,从根开始找到 \([x_0, x_0]\) 这个节点,将路径上每个节点的直线都求与 \(x = x_0\) 的交点,求这些点的纵坐标最大值即可。不难证明正确性,即,只有这些直线可能求得与 \(x = x_0\) 的最高交点。查询操作的复杂度为 \(O(\log M)\)

对于求最低交点(维护上凸壳)的情况,只需仿照上述流程讨论即可。

struct Line {
	ll k, b; Line(ll k = 0, ll b = +oo) : k(k), b(b) {  }
	inline ll f(int x) { return k * x + b; }
	inline double sect(Line a) { return 1.0 * (b - a.b) / (a.k - k); }
} ln[N];
int tot;
struct Tree {
	static constexpr int Q = N * 10;
	int id[Q], lc[Q], rc[Q], tot;
	inline void Ins(int &u, int l, int r, int t) {
		if(!u) u = ++tot, lc[u] = rc[u] = id[u] = 0;
        if(!id[u]) return id[u] = t, void();
		ll l1 = ln[id[u]].f(l), r1 = ln[id[u]].f(r);
        ll l2 = ln[t].f(l), r2 = ln[t].f(r);
		if(l1 <= l2 && r1 <= r2) return;
        if(l1 > l2 && r1 > r2) return id[u] = t, void();
		int mid = l + r >> 1;
        double x = ln[id[u]].sect(ln[t]);
        if(l1 < l2) swap(id[u], t);
		if(x <= mid) Ins(lc[u], lc[lst], l, mid, id[u]), id[u] = t;
		else Ins(rc[u], rc[lst], mid + 1, r, t);
	}
	inline ll Qry(int u, int l, int r, int x) {
		ll t = ln[id[u]].f(x);
        if(l == r || !id[u]) return t;
        int mid = l + r >> 1;
		return min(t, x <= mid ? Qry(lc[u], l, mid, x) : Qry(rc[u], mid + 1, r, x));
	}
} tr;

为了避免查询时特判,可以直接将第 \(0\) 条直线设为 \(y = 0 \times x + (-\infty)\)(若所求为纵坐标最大值)。

显然,李超树支持可持久化(但不支持查询两个版本间的增量),也可以撤销操作(只需用栈记录每次修改,在撤销时回退即可)。(例题:CF1175G Yet Another Partiton Problem

对于插入线段的情况,只需先找到完全包含于线段左右端点 \([L, R]\) 内的 \(O(\log M)\) 个线段树节点,从这些节点开始分别插入“直线”即可。这样做的时间复杂度为 \(O(n \log^2 M)\),空间复杂度为 \(O(n \log M)\)

P4655 [CEOI 2017] Building Bridges

\(n\) 根柱子依次排列,第 \(i\) 根柱子的高度为 \(h_i\)。在第 \(i\) 根和第 \(j\) 根柱子之间搭桥的花费为 \((h_i - h_j)^2\)。没有参与搭桥的柱子会被拆除,花费为 \(w_i\)(注意 \(w_i\) 可能是负数)。你需要求出通过搭桥把第 \(1\) 根珠子和第 \(n\) 根柱子连通的最小代价。注意,桥梁除端点外不能相交。\(n \le 10^5\)

\(f_i\) 表示连通 \(1\)\(i\) 的最小代价,\(pre\) 表示 \(w\) 的前缀和,则

\[\begin{aligned} f_i &= \min_{j = 0}^{i - 1} f_j + (h_i - h_{j})^2 + (pre_{i - 1} - pre_{j}) \\ &= pre_{i - 1} + h_i^2 + \min_{j = 0}^{i - 1} (-2h_{j} \times h_i + h_j^2 - pre_j) \end{aligned} \]

直线形式为 \(y = -2h_i \times x + (h_i^2 - pre_i)\),然而 \(h_i\) 不一定单调,所以不能用栈或队列来维护。于是使用插入直线的李超树进行优化即可。复杂度为 \(O(n \log M)\)

练习

P5468 [NOI2019] 回家路线

\(n\) 个车站和 \(m\) 班列车,第 \(i\) 号列车会在 \(p_i\) 时刻从 \(x_i\) 出发,在 \(q_i\) 时刻到达 \(y_i\),你只能在出发时刻在出发站点登上一辆列车,且列车到达后会立即下车。在一次换乘时等待 \(t\) 时间会产生 \(At^2 + Bt + C\) 的烦躁值,最终的烦躁值为从 \(1\) 号车站到达 \(n\) 号车站的总用时再加上换乘时产生的总烦躁值。你需要最小化这个值。\(n \le 10^5, m \le 2 \times 10^5\)

不妨将所有列车按 \((q_i, p_i)\) 排序,然后按顺序考虑每一趟列车,并且设 \(f_{u, i}\) 表示通过“所有终点站为 \(u\) 的列车中的第 \(i\) 趟”到达 \(u\) 的最小烦躁值(容易发现,这样的总状态数是 \(O(m)\) 的),则有

\[\begin{aligned} f_{u, i} &= \min_{q_{v, j} \le p_{u, i}} f_{v, j} + t_{u, i} + A(p_{u, i} - q_{v, j})^2 + B(p_{u, i} - q_{v, j}) + C \\ &= t_{u, i} + Ap^2_{u, i} + Bp_{u, i} + \min_{q_{v, j} \le p_{u, i}} (- 2Aq_{v, j}p_{u, i} + (f_{v, j} + Aq_{v, j}^2 - Bq_{v, j} + C)) \\ \end{aligned} \]

\(k_{v, j} = -2Aq_{v, j}, H_{v, j} = f_{v, j} + Aq_{v, j}^2 - Bq_{v, j} + C\),则后面的形式为求 \(v\) 车站前若干条直线与 \(x = p_{u, i}\) 相交的最小纵坐标,故需要维护上凸壳。由于对于同一车站,\(q_{v, j}\) 递增,所以 \(k_{v, j}\) 递减,所以新插入直线的最优范围是一段后缀;而对于一个 \(v\),由于我们按到达时间递增考虑每趟车,所以它作为起点时查询的 \(p_{u, i}\) 是递增的。这就可以用单调队列维护了。

然而不想考虑上面这个单调性怎么办?直接使用可持久化李超树,对每个点存储截止每次到达后的凸壳,这样就可以直接二分一个前缀版本转移了。这样做的时空复杂度均为 \(O(m \log m)\)

我不会告诉你我模拟赛的时候忘了斜率优化怎么写所以写出了这个惊世做法

#define ep emplace_back
#define all(x) begin(x), end(x)
using ll = long long;
constexpr int N = 200003, M = 1000;
constexpr ll oo = 1e18;
int n, m, A, B, C, ptr[N];
struct E { int u, v, t1, t2; } a[N];
vector<vector<int>> vec, rt; vector<vector<ll>> f;

struct Line {
	ll k, b; Line(ll k = 0, ll b = +oo) : k(k), b(b) {  }
	inline ll f(int x) { return k * x + b; }
	inline double sect(Line a) { return 1.0 * (b - a.b) / (a.k - k); }
} ln[N]; int tot;
struct Tree {
	static constexpr int Q = N * 20;
	int id[Q], lc[Q], rc[Q], tot;
	inline int Copy(int x) { return ++tot, tie(id[tot], lc[tot], rc[tot]) = tie(id[x], lc[x], rc[x]), tot; }
	inline void Ins(int &u, int lst, int l, int r, int t) {
		u = Copy(lst); if(!id[u]) return id[u] = t, void();
		ll l1 = ln[id[u]].f(l), r1 = ln[id[u]].f(r), l2 = ln[t].f(l), r2 = ln[t].f(r);
		if(l1 <= l2 && r1 <= r2) return; if(l1 > l2 && r1 > r2) return id[u] = t, void();
		int mid = l + r >> 1; double x = ln[id[u]].sect(ln[t]); if(l1 < l2) swap(id[u], t);
		if(x <= mid) Ins(lc[u], lc[lst], l, mid, id[u]), id[u] = t;
		else Ins(rc[u], rc[lst], mid + 1, r, t);
	}
	inline ll Qry(int u, int l, int r, int x) {
		ll t = ln[id[u]].f(x); if(l == r || !id[u]) return t; int mid = l + r >> 1;
		return min(t, x <= mid ? Qry(lc[u], l, mid, x) : Qry(rc[u], mid + 1, r, x));
	}
} tr;

main() {
	// Fin("route.in"), Fout("route.out");
	rd(n), rd(m), rd(A), rd(B), rd(C);
	for(int i = 1; i <= m; ++i)
		rd(a[i].u), rd(a[i].v), rd(a[i].t1), rd(a[i].t2);
	sort(a + 1, a + m + 1,
	[&](const E &a, const E &b) -> bool {
		return a.t2 == b.t2 ? a.t1 < b.t1 : a.t2 < b.t2;
	} );
	vec.resize(n + 1), f.resize(n + 1);
	rt.resize(n + 1), vec[1].ep(0);
	for(int i = 1; i <= m; ++i) vec[a[i].v].ep(a[i].t2);
	for(int i = 1; i <= n; ++i)
		f[i].resize(vec[i].size()), rt[i].resize(vec[i].size()), ptr[i] = -1;
	f[1][++ptr[1]] = 0, ln[++tot] = Line(0, 0), tr.Ins(rt[1][0], 0, 0, M, tot);
	for(int i = 1; i <= m; ++i) {
		int u = a[i].u, v = a[i].v, t1 = a[i].t1, t2 = a[i].t2;
		int pos = upper_bound(all(vec[u]), t1) - begin(vec[u]) - 1;
		++ptr[v], rt[v][ptr[v]] = ptr[v] ? rt[v][ptr[v] - 1] : 0;
		if(pos == -1) { f[v][ptr[v]] = +oo; continue; }
		f[v][ptr[v]] = tr.Qry(rt[u][pos], 0, M, t1) + A * t1 * t1 + B * t1 + C;
		ln[++tot] = Line(-2 * A * t2, f[v][ptr[v]] + A * t2 * t2 - B * t2);
		tr.Ins(rt[v][ptr[v]], rt[v][ptr[v]], 0, M, tot);
	}
	ll res = +oo;
	for(int i = 0; i <= ptr[n]; ++i)
		res = min(res, f[n][i] + vec[n][i]);
	writeln(res);
}

注意,这份代码无法通过加强版。

posted @ 2026-01-10 23:13  Neutral1sed  阅读(70)  评论(0)    收藏  举报