斜率优化初步
怎么说我还是菜,现在还不会这东西,真丢脸
我看你是完全不懂哦
Part 1 基于单调性的线性做法
例题一、P3195 [HNOI2008]玩具装箱TOY
这题目转移很明显
行8,整理一下好看点
设
斜率优化之思想
我们现在考虑到
或者说会有什么特殊的代数关系么?
我们现在只知道
好吧实际上我们采用的策略是把带
赶脚没问题了,我们知道斜率是
右边那个式子长得也很像(好吧其实就是)
不如令
每一个决策点都可以对应到一个点
同时根据我们推出来的那个柿子
一旦
另外一个性质:见下图
如果决策
一旦
同时说明一旦
此时已知三条图中线段斜率大小相对关系:
从左到右四个位置,枚举一下
会得到最优分别是:
也就是说无论如何都轮不到
所以我们维护的可用决策点实际上组成一个下凸壳(因为在上面的转移都更差),斜率单调不减(相邻的斜率可以相等!!!)
其实这还基于另一个事实:
因为那个
这就有一个更重要的性质:若决策点
因为斜率的限制是越来越放宽的。
斜率优化之实现
思想我们已经清楚:维护一个决策点凸包,每次找到最优决策点,直接转移。
问题是我们怎么维护凸包,怎么找最优决策点。
所以我们需要用两个到上文的性质
1
若决策点
s 在第i 次决策被发现差于决策点t ,那么此后的决策中s 永远不可能优于t .
说实话,凸包有点像单调队列不是吗?
当一个决策点又比你小又比你强,你就可以退役了。
所以活下来的都是比强者菜一点点的年轻 oier。
当我们完成了
我们就要把比它大,又比它菜的转移点弹出。
老年退役选手
具体操作方式就是按照之前那张图来。
2
但维护凸包的过程也不完全是单调队列。
我们把凸包最左下的决策点当成老年强者。
按照我们先前的式子
一旦
k <2a[i] 那就说明这个j_2 转移更优秀
因为
所以
长江后浪推前浪,浮事新人换旧人。
具体操作方式就是每次转移前检查 front 是否被超越
#include<cstdio>
#include<cstring>
#include<ctime>
#include<cstdlib>
#define rg register
#define il inline
#define MX (50000 + 5)
#define ll long long
#define db double
ll S[MX] ,dp[MX] ,a[MX] ,b[MX] ,Q[MX];
il double slope(int j1 ,int j2){
return (db)(dp[j2] + b[j2] * b[j2] - dp[j1] - b[j1] * b[j1]) / (b[j2] - b[j1]);
}
int main(){
int n ,l;
scanf("%d%d" ,&n ,&l);
for(rg int i = 1 ,tmp ; i <= n ; ++i){
scanf("%d" ,&tmp);
S[i] = S[i - 1] + tmp;
a[i] = S[i] + i;
b[i] = S[i] + i + l + 1;
}
b[0] = (l + 1);
int head = 0 ,tail = 0; //从0开始
//Q[0] = 0;
for(rg int i = 1 ; i <= n ; ++i){
while(head < tail && slope(Q[head] ,Q[head + 1]) < 2 * a[i]) ++head;
// 检查 front 是否是最优
dp[i] = dp[Q[head]] + (a[i] - b[Q[head]]) * (a[i] - b[Q[head]]);
while(head < tail && slope(Q[tail] ,i) < slope(Q[tail - 1] ,Q[tail])) --tail;
// 尾部插入,单调队列
Q[++tail] = i;
}
printf("%lld\n" ,dp[n]);
return 0;
}
论斜率优化的另一种推柿子方法
上文方法在某些题目中会受限。
例如斜率不单调、正负性不确定。
所以留坑
例题二、[USACO08MAR]土地征用Land Acquisition
按理来说这题应该更用来做例题
先考虑贪心,把无用的土地(比另一块土地
具体怎么做?排序,按
我们已经保证
此时盲猜一个结论,必须一段一段的买才会最优。
假设此时只有三块土地,已经排好序。
三块一起买就是
打乱顺序先买
显然一起买更优,我们可以通过数学归纳法推广到任意情况。
于是可以直接写出方程
显然是一个斜率优化的方程(而且比较裸)
令
因为我们已经排过序,所以保证
令
则
然后就是了,按照上题的分析我们可以得出我们要维护一个上凸壳
大于小于反过来就是了
注意一下我程序这里用的柿子是
#include<cstring>
#include<ctime>
#include<cstdlib>
#include<iostream>
#include<cstdio>
#include<algorithm>
#define rg register
#define il inline
#define MX (50000 + 4)
#define ll long long
#define db double
struct dirt{
int x ,y;
bool operator <(const dirt b)const{
return x == b.x ? y > b.y : x > b.x;
}
}d[MX];
ll dp[MX] ,Q[MX];
double slope(int j1 ,int j2){
return (db)(dp[j1] - dp[j2]) / (d[j2 + 1].x - d[j1 + 1].x);
}
int main(){
int n ,cnt = 0;;
scanf("%d" ,&n);
for(rg int i = 1 ; i <= n ; ++i){
scanf("%d%d" ,&d[i].x ,&d[i].y);
}
std::sort(d + 1 ,d + 1 + n);
int lasty = -1;
for(rg int i = 1 ; i <= n ; ++i){
if(d[i].y <= lasty) continue;
else lasty = d[i].y ,d[++cnt] = d[i];
}
int head = 0 ,tail = 0;
for(rg int i = 1 ; i <= cnt ; ++i){
while(head < tail && (d[i].y >= slope(Q[head] ,Q[head + 1]))) ++head;
dp[i] = dp[Q[head]] + 1LL * d[Q[head] + 1].x * d[i].y;
while(head < tail && (slope(Q[tail] ,i) <= slope(Q[tail - 1] ,Q[tail]))) --tail;
Q[++tail] = i;
}
printf("%lld\n" ,dp[cnt]);
return 0;
}
例题3 [NOI2019]回家路线
设 dp[i] 表示现在刚到第
首先我们可以yy出一个
显然发现一个事:这式子是斜优
愉快拆拆拆,假设一个转移
等等,每一个转移又有一个
惨痛的事实证明你按哪一个排都没用
我们可以把一个转移拆成 【在
这样我们就可以正常排序,只需记录操作类型
还发现一件事,这不是一维序列上的 dp ,凸包到底怎么维护?
对于每一个终点都维护一个凸包用来转移就是了
即【在时间
我们得维护
于是我们毫无顾虑地愉快拆拆拆式子,假设一个转移
这就是式子
下小上大倒三角,我们需要维护一个下凸包
单调性证明同例题1
贴个代码
#include<cstdio>
#include<cstring>
#include<deque>
#include<iostream>
#include<algorithm>
#define rg register
#define il inline
#define MX (200000 + 5)
#define db double
#define INF (1LL << 50)
#define ll long long
int n ,m ,A ,B ,C;
il int read()
ll min(ll a ,ll b)
struct edge{
int x ,y ,p ,q;
long long val;
}e[MX];
struct operation{
int pos ,tim ,id ,belong;
bool operator <(const operation &B)const{return tim == B.tim ? id > B.id : tim < B.tim;}
}op[MX * 2];
std::deque<int> que[MX >> 1];
ll dp[MX * 2];
db slope(int j1 ,int j2){
if(op[j1].tim - op[j2].tim == 0) return INF;
return (dp[j1] - dp[j2] * 1.0) / (op[j1].tim - op[j2].tim);
}
int main(){
n = read(); m = read();
A = read(); B = read(); C = read();
for(rg int i = 1 ,x ,y ,p ,q ; i <= m ; ++i){
x = read(); y = read();
p = read(); q = read();
e[i] = (edge){x ,y ,p ,q ,0};
op[i * 2 - 1] = (operation){x ,p ,1 ,i};
op[i * 2] = (operation){y ,q ,2 ,i};
}
ll ans = INF;
std::sort(op + 1 ,op + 1 + m + m);
e[0] = (edge){0 ,1 ,0 ,0};
op[0] = (operation){1 ,0 ,2 ,0};
que[1].push_back(0);
for(int i = 1 ,now ,type ; i <= m * 2 ; ++i){
now = op[i].pos ,type = op[i].id;
if(type == 1){
while(que[now].size() > 1){
int j1 = que[now].front() ,j2 = que[now].at(1);
ll t = 1LL * A * (2 * op[i].tim - op[j1].tim - op[j2].tim) + B;
if(slope(j1 ,j2) <= t) que[now].pop_front();
else break;
}
if(!que[now].empty()){
int j = que[now].front();
e[op[i].belong].val = dp[j] +
1LL * A * (op[i].tim - op[j].tim) * (op[i].tim - op[j].tim)
+ 1LL * B * (op[i].tim - op[j].tim) + C;
}
else{e[op[i].belong].val = INF; continue;}
}
else{
dp[i] = e[op[i].belong].val;
while(que[now].size() > 1){
int j1 = que[now].back() ,j2 = que[now].at(que[now].size() - 2);
if(slope(j1 ,i) < slope(j2 ,j1)) que[now].pop_back();
else break;
}
if(now == n){
ans = min(ans ,dp[i] + op[i].tim);
continue;
}
if(dp[i] < INF) que[now].push_back(i);
}
}
printf("%lld\n" ,ans);
return 0;
}
一些细节
小心算斜率的时候
建议就记
不要把 Que[tail] 写成 tail
三个决策点在同一直线时要放弃中间那个。
Part 2 二分决策点
流坑代填
P5785 [SDOI2012]任务安排
弱化版P2365 任务安排,暴力可过,没有负数。
好吧,当我们发现柿子