斜率优化-dp

\(\text{luogu-10979}\)

感觉斜率优化非常神秘啊。弱化版 luogu-2365。

设状态 \(f_i\) 表示完成前 \(i\) 个人任务分组后的最小费用,这里 \(T_i,C_i\) 都表示前缀和,那么有:

\[f_i = \min_{j < i} \{ f_j + T_i \times (C_i - C_j) + s \times (C_n - C_j) \} \]

这里有一个费用前置的思想,因为一次分组的启动时间对后续任务都有影响,于是直接把对后续的影响算进 \(f_i\) 里即可。这个式子是 \(O(n^2)\) 的,考虑怎么优化。

把式子拆开,合并同类项,得到:

\[f_i = \min_{j < i} \{ f_j - (s + T_i) \cdot C_j \} + T_i \cdot C_i + s \cdot C_n \]

这个式子的复杂度瓶颈在于找到最优决策点 \((C_j,f_j)\),我们考虑两个决策点 \(x,y\),若点 \(x\) 优于 \(y\) 则:

\[\begin{align} f_x - (s + T_i) \cdot C_x &< f_y - (s + T_i) \cdot C_y \\ (C_y - C_x) \cdot (s + T_i) &< f_y - f_x \end{align} \]

我们钦定 \(C_y > C_x\),则:

\[\frac{f_y - f_x}{C_y - C_x} > s + T_i \]

用单调队列维护下凸壳的时候,若 \(\frac{f_{l+1}-f_l}{C_{l+1}-C_l} \le s + T_i\) 则把队头 \(l\) 弹出,因为不优。

加进新的决策点的时候也要判断一下,若 \(\frac{f_r - f_{r-1}}{C_r - C_{r-1}} \ge \frac{f_i - f_{r}}{C_i - C_{r}}\),即新加进的决策点比队尾斜率小,那么队尾 \(r\) 就要被弹出了,因为以后都不可能成为答案。

这样每时刻的队头都是最优决策点,通过队头算出 \(f_i\) 即可。

\(\text{luogu-5785}\)

这是 luogu-10979 的强化版。区别在于本题 \(T_i\) 可能为负。

上一题因为 \(T_i\) 为非负整数,则前缀和 \(s+T_i\) 单调递增,于是可以通过不断弹出队头,来达到每时刻的队头都是最优决策点,本质上是维护了半个下凸壳。

本题就不一样了,因为前缀和 \(T_i\) 不满足单调递增,于是下凸壳的任何一个点都有可能成为最优决策点。

于是我们维护完整的下凸壳,每次查询时用二分解决,找到满足 \(\frac{f_{j+1}-f_j}{C_{j+1}-C_j} > s + T_i\) 的最小的 \(j\) 即可。

这个细节还挺多的,调了很久,放一下代码吧。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 300005
#define i128 __int128

ll n, s, t[N], c[N], f[N], q[N];

ll find(ll l, ll r, ll x) {
    ll res = r;
	while(l <= r) {
		ll mid = (l + r) >> 1;
		if((i128)(f[q[mid + 1]] - f[q[mid]]) > (i128)x * (c[q[mid + 1]] - c[q[mid]])) r = mid - 1, res = q[mid];
		else l = mid + 1;
	}
	return res;
} 

int main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	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];
	ll l = 0, r = 0; // 这里 l 不能初始化为 1,因为 0 也是一个决策点,代表 (0,0),类似哨兵节点。
    for(int i = 1; i <= n; i ++) {
		ll j = find(l, r, s + t[i]);
		f[i] = f[j] - (s + t[i]) * c[j] + t[i] * c[i] + s * c[n];
		while(l < r && (i128)(f[q[r]] - f[q[r - 1]]) * (c[i] - c[q[r]]) 
			>= (i128)(f[i] - f[q[r]]) * (c[q[r]] - c[q[r - 1]])) r --;
		q[++ r] = i;
	}
	cout << f[n] << "\n";
	return 0;
}

\(\text{atcoder-abc228h}\)

这个转化我没想到,可能是因为没做过斜率优化的题,做了两道之后感觉都类似先分组之类的设计状态。

对于这题来说就是先按 \(a_i\) 从小到大排个序,设计 \(f_i\) 表示把前 \(i\) 个分好组后的最小代价,这里的分组就是说每组都变成一组中最大的数,有转移式子:

\[f_i = \min_{j < i} \left\{ f_j + \left( \sum_{k=j+1}^i c_k \cdot (a_i - a_k) \right) + x \right\} \]

稍微转化一下可以得到:

\[f_i = \min_{j < i} \left\{ f_j + a_i \cdot \sum_{k=j+1}^i c_k - \sum_{k=j+1}^i c_k \cdot a_k + x \right\} \]

可以用前缀和优化一下,令 \(b_i = \sum\limits_{j=1}^i c_j, d_i = \sum\limits_{j=1}^i c_j \cdot a_j\),得到:

\[\begin{align} f_i = \min_{j < i} \left\{ f_j + d_j - a_i \cdot b_j \right\} + a_i \cdot b_i - d_i + x \end{align} \]

于是这里的决策点就是 \((b_j,f_j+d_j)\),参考 luogu-10979 的推导过程,若决策点 \(x\) 优于 \(y\),则满足:

\[\frac{f_y + d_y - f_x - d_x}{b_y - b_x} > a_i \]

所以斜率就是 \(\frac{f_y + d_y - f_x - d_x}{b_y - b_x}\),跟 luogu-10979 一样维护就好,因为这里 \(a_i\) 也是单调不减。

\(\text{luogu-2900}\)

设计 \(f_i\) 表示前 \(i\) 块土地分好组的最小花费,于是有转移:

\[f_i = \min_{j < i} \left\{ f_j + \max_{k=j+1}^i w_k \times \max_{k=j+1}^i l_k \right\} \]

然后就会发现这是个啥玩意,斜率优化完全没办法解决啊。

考虑怎么消掉这两个 \(\max\),考虑到若 \(w_i \ge w_j,l_i \ge l_j\),那么 \(j\) 这块就是没用的,不考虑这样的 \(j\) 即可。

那么剩下的块排序得到满足 \(w_{i-1} > w_i,l_{i-1} < l_i\) 的一个序列,这样转移就变成了:

\[f_i = \min_{j < i} \{ f_j + w_{j+1} \cdot l_i \} \]

然后考虑斜率优化,若 \(x\) 优于 \(y\),则:

\[\begin{align} f_x + w_{x+1} \cdot l_i &< f_y + w_{y + 1} \cdot l_i \\ f_x - f_y &< l_i \cdot (w_{y + 1} - w_{x + 1}) \end{align} \]

我们钦定 \(w_{y + 1} > w_{x + 1}\),则:

\[\frac{f_y - f_x}{w_{y + 1} - w_{x + 1}} > -l_i \]

维护一个下凸壳就可以。

只能说这题非常需要注意符号,还是不要用上面那个式子了,用这个,不然比较斜率的时候很容易出错:

\[\frac{f_x - f_y}{w_{y + 1} - w_{x + 1}} > l_i \]

\(\text{luogu-3195}\)

真正的模板题。

设计 \(f_i\) 表示前 \(i\) 件玩具分完组的最小代价,则朴素的转移就是:

\[f_i = \min_{j < i} \left\{ f_j + \left( i - j - 1 + \sum_{k=j+1}^i c_k - L \right)^2 \right\} \]

稍微化简一下,令 \(b_i = \sum\limits_{j=1}^i c_j,d_i = i + b_i,L' = L + 1\),则:

\[f_i = \min_{j < i} \{ (f_j + d_j^2 + 2d_jL') - 2d_j \cdot d_i \} + (d_i - L')^2 \]

考虑斜率优化,若 \(x\) 优于 \(y\),则:

\[\begin{align} (f_x + d_x^2 + 2d_xL') - 2d_x \cdot d_i &< (f_y + d_y^2 + 2d_yL') - 2d_y \cdot d_i \\ d_i \cdot (2d_y - 2d_x) &< f_y + d_y^2 + 2d_yL' - f_x - d_x^2 - 2d_xL' \end{align} \]

我们钦定 \(d_y > d_x\),则:

\[\frac{f_y + d_y^2 + 2d_yL' - f_x - d_x^2 - 2d_xL'}{2d_y - 2d_x} > d_i \]

其中 \(d_i\) 单调递增,可以直接斜率优化写。

\(\text{luogu-3628}\)

设计 \(f_i\) 表示前 \(i\) 名士兵分好组的最大战斗力,则有转移:

\[f_i = \max_{j < i} \left\{ f_j + a\left( \sum_{k=j+1}^i x_k \right)^2 + b\sum_{k=j+1}^i x_k + c \right\} \]

\(p_i = \sum\limits_{j=1}^i x_j\),则:

\[f_i = \max_{j < i} \{ (f_j + a \cdot p_j^2 - b \cdot p_j) - 2a \cdot p_j \cdot p_i \} + a \cdot p_i^2 + b \cdot p_i + c \]

考虑斜率优化,若 \(x\) 优于 \(y\),则:

\[\begin{align} (f_x + a \cdot p_x^2 - b \cdot p_x) - 2a \cdot p_x \cdot p_i &> (f_y + a \cdot p_y^2 - b \cdot p_y) - 2a \cdot p_y \cdot p_i \\ p_i \cdot 2a(p_y - p_x) &> f_y + a \cdot p_y^2 - b \cdot p_y - f_x - a \cdot p_x^2 + b \cdot p_x \end{align} \]

我们钦定 \(p_y > p_x\),由于 \(a < 0\),则:

\[\frac{f_y + a \cdot p_y^2 - b \cdot p_y - f_x - a \cdot p_x^2 + b \cdot p_x}{2a(p_y - p_x)} > p_i \]

其中 \(p_i\) 单调递增,可以直接斜率优化写。

\(\text{luogu-3648}\)

这道题有些许不一样,必须要切 \(k\) 刀,而且要输出方案,方案比较好解决,记录一下前驱即可。

考虑怎么处理恰好切 \(k\) 刀,我们可以设计 \(f_{i,j}\) 表示处理完前 \(i\) 个切了 \(j\) 刀的最大贡献:

\[f_{i,j} = \max_{t < i} \{ f_{t,j-1} + s_t (s_i - s_t) \} \]

其中 \(s_i = \sum\limits_{j=1}^i a_j\),用滚动数组变成一维状态,辅助数组为 \(g\),变换一下形式:

\[f_{i} = \max_{j < i} \{ (g_j - s_j^2) + s_j s_i \} \]

考虑斜率优化,若 \(x\) 优于 \(y\),则:

\[\begin{align} (g_x - s_x^2) - s_x s_i &> (g_y - s_y^2) - s_y s_i \\ s_i (s_y - s_x) &> g_y - s_y^2 - g_x + s_x^2 \end{align} \]

我们钦定 \(s_y \ge s_x\),则:

\[\frac{g_y - s_y^2 - g_x + s_x^2}{(-s_y) - (-s_x)} > s_i \]

其中 \(s_i\) 单调不降,可以直接斜率优化写,注意 \(s_y - s_x\) 有可能为 \(0\),要返回 \(-\infty\),也就是不入队。

\(k\) 遍就能求出 \(f_{n,k}\),过程中记录一下前驱。

\(\text{luogu-4360}\)

这题不需要 dp,可以直接写式子。

\(s_i = \sum\limits_{j=1}^i w_j,dis_i = \sum\limits_{j=i}^n d_j,sum = \sum\limits_{j=1}^n w_j \cdot dis_j\),则答案为:

\[ans = \min_{j<i} (sum - s_j \cdot dis_j - (s_i - s_j) \cdot dis_i) \]

考虑斜率优化,若 \(x\) 优于 \(y\),则:

\[\begin{align} sum - s_x \cdot dis_x - (s_i - s_x) \cdot dis_i &< sum - s_y \cdot dis_y - (s_i - s_y) \cdot dis_i \\ s_y \cdot dis_y - s_x \cdot dis_x &< dis_i \cdot (s_y - s_x) \end{align} \]

我们钦定 \(s_y > s_x\),则:

\[-\frac{s_y \cdot dis_y - s_x \cdot dis_x}{s_y - s_x} > -dis_i \]

其中 \(-dis_i\) 单调递增,可以维护下凸壳。

\(\text{luogu-5468} / \text{luogu6302}\)

感觉挺困难的一道题。

这个状态设计不允许我们加入时间维度,还是先按照常规的设计方式,\(f_i\) 表示经过了 \(i\) 班列车的最小代价:

\[f_i = \min_{y_j = x_i \wedge q_j \le p_i} \{ f_j + A(p_i - q_j)^2 + B(p_i - q_j) + C \} \]

答案即为 \(\min f_i + q_i\),把上面的式子化成标准格式:

\[f_i = \min_{y_j = x_i \wedge q_j \le p_i} \{ (f_j + Aq_j^2 - Bq_j) - 2A q_j \cdot p_i \} + Ap_i^2 + Bp_i + C \]

考虑斜率优化,若 \(x\) 优于 \(y\),则:

\[\begin{align} (f_x + Aq_x^2 - Bq_x) - 2A q_x \cdot p_i &< (f_y + Aq_y^2 - Bq_y) - 2A q_y \cdot p_i \\ p_i \cdot 2A(q_y - q_x) &< f_y + Aq_y^2 - Bq_y - f_x - Aq_x^2 + Bq_x \end{align} \]

我们钦定 \(q_y > q_x\),则:

\[\frac{f_y + Aq_y^2 - Bq_y - f_x - Aq_x^2 + Bq_x}{2A(q_y - q_x)} > p_i \]

下面是重点:

有了这个式子不能直接做啊,我们还有 \(y_j=x_i \wedge q_j \le p_i\) 的限制。

第一个限制其实好解决,可以开 \(n\) 个单调队列,类似维护了 \(n\) 个下凸壳的感觉?

插入决策点的时候加入到 \(y_j\) 对应的单调队列里,选择决策点的时候从 \(x_i\) 对应的队列里选取即可。

第二个限制比较难处理,大致思路就是我们用双指针维护两条时间线,结构体数组 \(a_i\) 用来存储按出发时间 \(p\) 排序的时间线;\(b_i\) 用来存储按到达时间 \(q\) 排序的时间线,这样我们就有了两条独立的时间线:一条是 dp 计算顺序(出发时间),一条是决策点激活顺序(到达时间)。

维护指针 \(j\) 指向 \(b\) 中最后一个合法的决策点,每次转移前把合法的决策点加进对应的队列中,这样就能满足当前可选择的决策点满足 \(q_j \le p_i\)

处理过程中需要用到 \(a,b\) 数组的互相转换,所以需要记录一下 \(b \to a\) 的映射关系。

实现细节比较多,贴一下代码。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N 1000005
#define INF 1e18
#define Y(i) (f[mp[i]] + A * b[i].q * b[i].q - B * b[i].q)
#define X(i) (2 * A * b[i].q)

ll n, m, A, B, C, l[N], r[N], ct[N], pos[N], mp[N], f[N];
struct node { ll x, y, p, q, id; } a[N], b[N];
vector<ll> q[N];

double calc(ll x, ll y) {
    // 这里判断条件一定不能写 b[x].q == b[y].q,因为 A 可能为 0!
	if(X(x) == X(y)) return (Y(x) < Y(y) ? INF : -INF);
	return 1.0 * (Y(y) - Y(x)) / (X(y) - X(x));
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin >> n >> m >> A >> B >> C; f[N - 4] = INF, mp[N - 4] = N - 4;
	for(int i = 1; i <= m; i ++)
		cin >> a[i].x >> a[i].y >> a[i].p >> a[i].q, 
		a[i].id = i, ct[a[i].y] ++, b[i] = a[i];
	sort(a + 1, a + m + 1, [](node l, node r) { return l.p < r.p; });
	sort(b + 1, b + m + 1, [](node l, node r) { return l.q < r.q; });
	for(int i = 1; i <= n; i ++) q[i].resize(ct[i] + 5);
	for(int i = 2; i <= n; i ++) q[i][l[i] = r[i] = 1] = N - 4;
	for(int i = 1; i <= m; i ++) pos[a[i].id] = i;
	for(int i = 1; i <= m; i ++) mp[i] = pos[b[i].id];
	ll res = INF; l[1] = r[1] = 1;
	for(int i = 1, j = 0, t = 0; i <= m; i ++) {
		while(j < m && b[j + 1].q <= a[i].p) {
			t = b[++ j].y;
			while(l[t] < r[t] && calc(q[t][r[t] - 1], q[t][r[t]]) > calc(q[t][r[t]], j)) r[t] --;
			q[t][++ r[t]] = j;
		}
		t = a[i].x;
		while(l[t] < r[t] && calc(q[t][l[t]], q[t][l[t] + 1]) <= a[i].p) l[t] ++;
		ll k = q[t][l[t]];
		f[i] = f[mp[k]] + A * (a[i].p - b[k].q) * (a[i].p - b[k].q) + B * (a[i].p - b[k].q) + C;
		if(a[i].y == n) res = min(res, f[i] + a[i].q);
	}
	cout << res << "\n";
	return 0;
}
posted @ 2026-05-08 14:31  So_noSlack  阅读(15)  评论(0)    收藏  举报