斜率优化-dp
\(\text{luogu-10979}\)
感觉斜率优化非常神秘啊。弱化版 luogu-2365。
设状态 \(f_i\) 表示完成前 \(i\) 个人任务分组后的最小费用,这里 \(T_i,C_i\) 都表示前缀和,那么有:
这里有一个费用前置的思想,因为一次分组的启动时间对后续任务都有影响,于是直接把对后续的影响算进 \(f_i\) 里即可。这个式子是 \(O(n^2)\) 的,考虑怎么优化。
把式子拆开,合并同类项,得到:
这个式子的复杂度瓶颈在于找到最优决策点 \((C_j,f_j)\),我们考虑两个决策点 \(x,y\),若点 \(x\) 优于 \(y\) 则:
我们钦定 \(C_y > C_x\),则:
用单调队列维护下凸壳的时候,若 \(\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\) 个分好组后的最小代价,这里的分组就是说每组都变成一组中最大的数,有转移式子:
稍微转化一下可以得到:
可以用前缀和优化一下,令 \(b_i = \sum\limits_{j=1}^i c_j, d_i = \sum\limits_{j=1}^i c_j \cdot a_j\),得到:
于是这里的决策点就是 \((b_j,f_j+d_j)\),参考 luogu-10979 的推导过程,若决策点 \(x\) 优于 \(y\),则满足:
所以斜率就是 \(\frac{f_y + d_y - f_x - d_x}{b_y - b_x}\),跟 luogu-10979 一样维护就好,因为这里 \(a_i\) 也是单调不减。
\(\text{luogu-2900}\)
设计 \(f_i\) 表示前 \(i\) 块土地分好组的最小花费,于是有转移:
然后就会发现这是个啥玩意,斜率优化完全没办法解决啊。
考虑怎么消掉这两个 \(\max\),考虑到若 \(w_i \ge w_j,l_i \ge l_j\),那么 \(j\) 这块就是没用的,不考虑这样的 \(j\) 即可。
那么剩下的块排序得到满足 \(w_{i-1} > w_i,l_{i-1} < l_i\) 的一个序列,这样转移就变成了:
然后考虑斜率优化,若 \(x\) 优于 \(y\),则:
我们钦定 \(w_{y + 1} > w_{x + 1}\),则:
维护一个下凸壳就可以。
只能说这题非常需要注意符号,还是不要用上面那个式子了,用这个,不然比较斜率的时候很容易出错:
\(\text{luogu-3195}\)
真正的模板题。
设计 \(f_i\) 表示前 \(i\) 件玩具分完组的最小代价,则朴素的转移就是:
稍微化简一下,令 \(b_i = \sum\limits_{j=1}^i c_j,d_i = i + b_i,L' = L + 1\),则:
考虑斜率优化,若 \(x\) 优于 \(y\),则:
我们钦定 \(d_y > d_x\),则:
其中 \(d_i\) 单调递增,可以直接斜率优化写。
\(\text{luogu-3628}\)
设计 \(f_i\) 表示前 \(i\) 名士兵分好组的最大战斗力,则有转移:
令 \(p_i = \sum\limits_{j=1}^i x_j\),则:
考虑斜率优化,若 \(x\) 优于 \(y\),则:
我们钦定 \(p_y > p_x\),由于 \(a < 0\),则:
其中 \(p_i\) 单调递增,可以直接斜率优化写。
\(\text{luogu-3648}\)
这道题有些许不一样,必须要切 \(k\) 刀,而且要输出方案,方案比较好解决,记录一下前驱即可。
考虑怎么处理恰好切 \(k\) 刀,我们可以设计 \(f_{i,j}\) 表示处理完前 \(i\) 个切了 \(j\) 刀的最大贡献:
其中 \(s_i = \sum\limits_{j=1}^i a_j\),用滚动数组变成一维状态,辅助数组为 \(g\),变换一下形式:
考虑斜率优化,若 \(x\) 优于 \(y\),则:
我们钦定 \(s_y \ge s_x\),则:
其中 \(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\),则答案为:
考虑斜率优化,若 \(x\) 优于 \(y\),则:
我们钦定 \(s_y > s_x\),则:
其中 \(-dis_i\) 单调递增,可以维护下凸壳。
\(\text{luogu-5468} / \text{luogu6302}\)
感觉挺困难的一道题。
这个状态设计不允许我们加入时间维度,还是先按照常规的设计方式,\(f_i\) 表示经过了 \(i\) 班列车的最小代价:
答案即为 \(\min f_i + q_i\),把上面的式子化成标准格式:
考虑斜率优化,若 \(x\) 优于 \(y\),则:
我们钦定 \(q_y > q_x\),则:
下面是重点:
有了这个式子不能直接做啊,我们还有 \(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;
}
本文来自博客园,作者:So_noSlack,转载请注明原文链接:https://www.cnblogs.com/So-noSlack/p/19994241

浙公网安备 33010602011771号