Brilliant11001
2024-11-01 18:19:33
斜率优化是 dp 优化里比较神奇的一派,如果没学过,那么遇到了大概率是不会的,但是只要学过这种思想,那么很多题都可以触类旁通,迎刃而解。
这里主要介绍序列上的斜率优化 dp 模型。
其中与单调队列优化不同的是,单调队列优化的针对对象是多项式
斜率优化针对的是多项式
可见这是个序列问题,考虑将目前处理到第几位作为阶段来划分,即令
枚举最后一次分批在哪个位置,设为
所以最后一批任务即为
所以状态转移方程为:
其中
前
难点在于花费的计算和完成任务的时间有关,机器启动时间
用
综上所述:状态转移方程为:
时间复杂度为
#include <cstring>
#include <iostream>
using namespace std;
const int N = 5010;
typedef long long ll;
int n, S;
ll sumt[N], sumc[N];
ll dp[N];
int main() {
scanf("%d%d", &n, &S);
for(int i = 1; i <= n; i++) {
scanf("%d%d", &sumt[i], &sumc[i]);
sumt[i] += sumt[i - 1];
sumc[i] += sumc[i - 1];
}
memset(dp, 0x3f, sizeof dp);
dp[0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 0; j < i; j++)
dp[i] = min(dp[i], dp[j] + sumt[i] * (sumc[i] - sumc[j]) + S * (sumc[n] - sumc[j]));
printf("%lld", dp[n]);
return 0;
}
题目描述一样,但
对状态转移方程进行整理,合并同类项,得:
对于每个
顺便提一句动态规划的优化思路:
在朴素代码上做等价变形。
根据上文提到的方向
有点眼熟…… 再多看一眼…… 这不是直线斜截式
以
也就是说,决策候选集合是坐标系的一个点集,每个决策
这样一来,状态转移方程就变为了
因为要求的
形象一点来说就是有一根斜率为
如图所示,点
观察图像不难发现,只有当
算完
首先想到:可以用二分。观察,思考什么时候
可以发现,当
我们先用一个容器(这里选用队列)来存储目前的点集,然后二分求第一个和上一个点构成的直线斜率小于
然后用二分出的答案来计算
再插入决策
时,就应该将队尾弹出。
这样就可以将时间复杂度优化到
#include <iostream>
using namespace std;
const int N = 300010;
typedef long long ll;
int n, S;
ll sumt[N], sumc[N];
ll dp[N];
int q[N], hh, tt = -1;
int main() {
scanf("%d%d", &n, &S);
for(int i = 1; i <= n; i++) {
scanf("%lld%lld", &sumt[i], &sumc[i]);
sumt[i] += sumt[i - 1];
sumc[i] += sumc[i - 1];
}
q[++tt] = 0;
for(int i = 1; i <= n; i++) {
int l = hh, r = tt;
while(l < r) {
int mid = l + r >> 1;
if(dp[q[mid + 1]] - dp[q[mid]] > (sumt[i] + S) * (sumc[q[mid + 1]] - sumc[q[mid]])) r = mid;
else l = mid + 1;
}
dp[i] = dp[q[l]] - (S + sumt[i]) * sumc[q[l]] + sumt[i] * sumc[i] + S * sumc[n];
while(hh < tt && (__int128)(dp[q[tt]] - dp[q[tt - 1]]) * (sumc[i] - sumc[q[tt]]) >= (__int128)(dp[i] - dp[q[tt]]) * (sumc[q[tt]] - sumc[q[tt - 1]])) tt--;
q[++tt] = i;
}
printf("%lld", dp[n]);
return 0;
}
注意到这道题有个特殊条件:
外层循环每增加
这启示我们可以用单调队列来维护凸包。
建立一个单调队列
时,就应该将队头弹出;
取出队头
即将要把
时,就应该将队尾弹出。
时间复杂度为
#include <cstring>
#include <iostream>
using namespace std;
const int N = 300010;
typedef long long ll;
int n, S;
ll sumt[N], sumc[N];
int q[N], hh, tt = -1;
ll dp[N];
int main() {
scanf("%d%d", &n, &S);
for(int i = 1; i <= n; i++) {
scanf("%lld%lld", &sumt[i], &sumc[i]);
sumt[i] += sumt[i - 1];
sumc[i] += sumc[i - 1];
}
q[++tt] = 0;
for(int i = 1; i <= n; i++) {
//为防止浮点除法导致精度丢失,故交叉相乘,但要注意极端数据会爆 long long,需使用 __int128
while(hh < tt && (__int128)(dp[q[hh + 1]] - dp[q[hh]]) <= (__int128)(sumt[i] + S) * (sumc[q[hh + 1]] - sumc[q[hh]])) hh++;
int j = q[hh];
dp[i] = dp[j] - sumc[j] * (sumt[i] + S) + sumt[i] * sumc[i] + sumc[n] * S;
while(hh < tt && (__int128)(dp[q[tt]] - dp[q[tt - 1]]) * (sumc[i] - sumc[q[tt]]) >= (__int128)(dp[i] - dp[q[tt]]) * (sumc[q[tt]] - sumc[q[tt - 1]])) tt--;
q[++tt] = i;
}
printf("%lld", dp[n]);
return 0;
}
注意:为了防止除法丢失精度,可以交叉相乘。
然鹅,对于一些数据及其毒瘤的题,不仅斜率可能不单调递增,而且横坐标也可能不单调递增,我们分别来看一下这两种情况。
P5785 [SDOI2012] 任务安排
题意相同,但
目前没有遇到这种题。
横坐标不单增,意味着可能需要在中间某个地方插入点,但单调队列还是可以用的,这时可以在单调队列中二分插入,但这样单调队列就要写成“单调链表” 了,感觉不太好写。
P4655 [CEOI2017] Building Bridges
不得不说这种题真的毒瘤。
这时候就应该拿出斜率优化三件套了:李超线段树、cdq 分治、平衡树。
但是蒟蒻不会前两个,只能选择平衡树。
解释一下为什么是平衡树。
我们需要一个数据结构,支持以下操作:
综上所述,平衡树是最符合条件的数据结构。
我选择了最灵活的 Splay 来维护凸包。
#include <cmath>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010;
const double eps = 1e-10, inf = 1e15; //注意精度
typedef long long ll;
int n;
ll h[N], sumw[N], dp[N];
struct node{
int s[2], p;
double xs, ys, lk, rk;
void init(int p_, double xs_, double ys_) {p = p_, xs = xs_, ys = ys_;}
}tr[N];
int root, idx;
inline double X(int x) {return h[x];} //横坐标
inline double Y(int x) {return dp[x] + h[x] * h[x] - sumw[x];} //纵坐标
inline double slope(int i, int j) {
if(fabs(tr[i].xs - tr[j].xs) < eps) return tr[i].ys < tr[j].ys ? inf : -inf; //特判直线垂直于 x 轴的情况
return (tr[i].ys - tr[j].ys) / (tr[i].xs - tr[j].xs);
}
void rotate(int x) {
int y = tr[x].p, z = tr[y].p;
int k = x == tr[y].s[1];
tr[z].s[y == tr[z].s[1]] = x, tr[x].p = z;
tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
tr[x].s[k ^ 1] = y, tr[y].p = x;
}
inline void splay(int x, int k) {
while(tr[x].p != k) {
int y = tr[x].p, z = tr[y].p;
if(z != k) {
if((x == tr[y].s[1]) ^ (y == tr[z].s[1])) rotate(x);
else rotate(y);
}
rotate(x);
}
if(!k) root = x;
}
inline void insert(double xs, double ys) {
int u = root, p = 0;
while(u) p = u, u = tr[u].s[xs + eps > tr[u].xs];
u = ++idx;
if(p) tr[p].s[xs + eps > tr[p].xs] = u;
tr[u].init(p, xs, ys);
splay(u, 0);
}
inline int get_prev(int x) {
int res = 0, u = tr[x].s[0];
while(u) {
if (tr[u].lk < slope(u, x) + eps) res = u, u = tr[u].s[1]; //只寻找满足凸包性质的点
else u = tr[u].s[0];
}
return res;
}
inline int get_next(int x) {
int res = 0, u = tr[x].s[1];
while(u) {
if (slope(x, u) < tr[u].rk + eps) res = u, u = tr[u].s[0]; //只寻找满足凸包性质的点
else u = tr[u].s[1];
}
return res;
}
//以上为 Splay 模板
//维护凸壳
inline void update(int &rt, int x) {
splay(x, 0);
if (tr[x].s[0]) {
int y = get_prev(x);
if(y) { //有可能有左子树但左子树上的点都不满足凸包性质,所以要特判
splay(y, x);
tr[y].s[1] = 0; // 清空那些不满足凸包性质的点
tr[y].rk = tr[x].lk = slope(y, x); //计算斜率
}
else tr[x].s[0] = 0, tr[x].lk = -inf; //左边没有点能满足凸包性质,全部删掉且斜率为负无穷
} else tr[x].lk = -inf;
if (tr[x].s[1]) { //同上
int y = get_next(x);
if(y) {
splay(y, x);
tr[y].s[0] = 0;
tr[x].rk = tr[y].lk = slope(x, y);
}
else tr[x].s[1] = 0, tr[x].rk = inf;
} else tr[x].rk = inf;
if (tr[x].lk + eps > tr[x].rk) { //若左斜率大于右斜率,则说明不满足凸包性质,删掉
int y = get_prev(x), z = get_next(x);
splay(y, 0), splay(z, y);
tr[z].s[0] = 0;
tr[y].rk = tr[z].lk = slope(y, z); //重新更新斜率
}
}
//寻找最优决策
//在 Splay 上二分求解
//若当前斜率在左右斜率之间,则找到
//若当前点的左斜率小于当前斜率,说明要求的点在左侧,递归左子树
//否则在右侧,递归右子树
int get_strategy(int u, double k) {
if(!u) return 0;
if(tr[u].lk < k + eps && k < tr[u].rk + eps) return u;
if(tr[u].lk + eps > k) return get_strategy(tr[u].s[0], k);
else return get_strategy(tr[u].s[1], k);
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%lld", &h[i]);
for(int i = 1; i <= n; i++) {
scanf("%lld", &sumw[i]);
sumw[i] += sumw[i - 1];
}
dp[1] = 0;
dp[2] = dp[1] + (h[2] - h[1]) * (h[2] - h[1]);
insert(X(1), Y(1));
update(root, 1);
insert(X(2), Y(2));
update(root, 2); //先插入两个基本决策点,方便计算斜率
for(int i = 3; i <= n; i++) {
int j = get_strategy(root, 2.0 * h[i]); //获取最优决策
dp[i] = dp[j] + (h[i] - h[j]) * (h[i] - h[j]) + sumw[i - 1] - sumw[j]; //更新
insert(X(i), Y(i));
update(root, i); //插入新决策并维护凸包
}
printf("%lld", dp[n]);
return 0;
}
(纯纯毒瘤,调了一个上午 + 下午的一节课时间)
P3195 [HNOI2008] 玩具装箱
P4360 [CEOI2004] 锯木厂选址
P2120 [ZJOI2007] 仓库建设
P3648 [APIO2014] 序列分割
P2900 [USACO08MAR] Land Acquisition G
P4072 [SDOI2016] 征途
P3628 [APIO2010] 特别行动队
AT_dp_z Frog 3
P5785 [SDOI2012] 任务安排
P4655 [CEOI2017] Building Bridges
P4027 [NOI2007] 货币兑换