前言:
原来我以为数位 DP 已经够邪恶了
直到我遇见了他
毁灭你,与你有何相干。
这是之前寒假集训的最后一节课
而作者由于太蒻了
所以不能说是融会贯通吧至少也可以说是一窍不通
斜优是一个蒟蒻迈向牛马走向神犇的第一步
学会了就可以水好多好多蓝紫题啦!
所以闲话少叙,
前置知识:
能做出一道 DP 橙题
听说过平面直角坐标系
注:关于具体的时间复杂度证明和严格正确性分析
蒟蒻作者实在无能为力
这个好像貌似应该没啥用吧(bushi)(逃)(doge 保命)
所以基本都只会给出感性理解
Example 01[P3195 玩具装箱]
根据一位
题意简述:
给出一个数组
其中
Solution
首先考虑可以设
显然
容易想到朴素转移
这个转移是
肯定是会
思考怎么优化
蒟蒻貌似现在只学了一个单调队列优化
所以我们试试
但是发现有两个问题:
- 这个貌似没有什么可以淘汰的
当然可以直接维护最小值啦 - 关键是一个事情——单纯的
当然是可以维护
后面的部分如果不带平方也可以乱搞过
但这个平方……很难搞,非常难搞
平方是要拆的,但怎么拆,非常重要
如果我们直接暴力开干是非常麻烦的
我们根据古希腊掌管CCF的神的启示
将其按照和
当然我们给它起个名字
和
和
都没关系的叫
那么我们拆开得到
现在我们先不用管其中的这三个函数分别是啥
作者一开始就是总去先想这个函数
然后成功炸掉了自己的 CPU
接下来用刚才的方法再分一次
别蒙,第一步就是加法交换律和结合律
我们用大写的
得到
当我们现在发现了一个问题
其中有一项
那我们没有办法了
但为了统一,我们还是把他分成两个大写的函数
就
那我们就得到了一个式子
接下来我们暂时设当前的
则还可以去掉这个
直接就是
此时我们发现这个
于是最简式子即为
你不禁要问了:这一大套究竟有啥用呢?
其实并没有什么用,只是让你烧一会CPU而已
现在看一下我们最后得到的不等式
这个就是斜率优化的模板不等式
斜率优化就是通过一系列操作
将取最小值的复杂度搞到
现在我们终于可以来看看斜率是啥东西了
斜率,按某度某科的说法,是一条直线的倾斜程度
但用人话讲,他就是一条直线的一个参数
斜率的英文是
在一条直线上随意选两个点
用纵坐标之差减去横坐标之差
注意这个斜率是有正负的
所以这个不能瞎减
必须用同一个点作为被减数
但顺序却很无所谓
因为换个顺序分数上下都变成了原来的相反数
分数值并不会变
接下来再介绍一个概念:截距
截距是一条直线与 Y 轴交点的 Y 坐标
具体地讲,如果这条直线的解析式是
那么截距就是这个 b
接下来我们在把原来得到的式子转化一下
(此处先不用考虑正负号的问题,因为都可以搞到方程里)
我们发现这个和解析式
又由于
完全可以整体求出最后一项最小值之后再统计答案
然后我们发现这个式子中想要最小化的一项就是我们的截距 b
所以这相当于什么呢
相当于我们每遍历到一个数
都往点集里加了一个点
然后求的最小值相当于每次设定一个统一的斜率
求过这些点直线的最小截距
当然,暴力的想法是遍历每个点求一下
但这样肯定时间复杂度没有变,并不优
思考能否剔除掉一些没有用的点
显然有两种点是完全没有用的
首先是图中黄色线段所连接的两个点
如果他们横坐标相同,那么靠上的点一定不优
其次一种比较难想
见图中两条绿色线段连起来的三个点
严格地说,如果一个平面内有三个点
设
那么如果有
则
如图的直线
如果给出的斜率
否则靠左的
有了这个原理我们就可以看看了
这是我们刚才的那个三角形
可以看出来
那么我们可知
如果给出的斜率
如果给出的斜率
由此得证
这样搞出来的其实就是这个点集的下凸壳
我们可以满足所有可能的转移点点一定在这个下凸壳上
而且也一定满足这些凸壳上的点随着横坐标增大斜率单调递增
这个其实就可以用单调栈/单调队列来维护了
但搞出了凸壳说穿了还是常数优化
但是很多题除了已经列出的性质外还满足两个点
- 给出的点横坐标单调不降
- 给出的斜率同样单调不降
而这才是单调队列的用武之地
再看一眼线段 AB 的图片
我们发现随着给出斜率的增加左面的点越来越容易被淘汰
而如果后面的点已被淘汰那前面的就更白扯了
而当你一通淘汰之后剩下的对头即为最优答案
这就可以用单调队列了
其实有的题不满足如上性质,但也有一些方法进行处理,但作者暂时不会
好了,现在我们重新看一下这道题
这是我们的朴素转移式
我们把它拆开按上面的方法弄一下
这里我们把
原式变为
此时就基本结束了
我们看一下点的信息
我们发现 A,B 数组均是单调递增的完美符合性质
So Coding...
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
const int N=50098;
ll dp[N],A[N],B[N];
struct Point{ll x,y;int u;};
//dp[i]=dp[j]+B[i]^2-2B[i]A[j]+A[j]^2
//dp[j]+A[j]^2=B[i]*2A[j]+(dp[i]-B[i]^2)
//Point(2A[j],dp[j]+A[j]^2)
inline Point mp(ll x,ll y,int u){Point tmp;tmp.x=x,tmp.y=y,tmp.u=u;return tmp;}
inline ld S(Point a,Point b){return (ld)(a.y-b.y)/(ld)(a.x-b.x);}
inline ll Pow(ll x){return x*x;}
class Deque{
public:
Point front(){return k[ff];}
Point fnxt(){return k[ff+1];}
Point back(){return k[tt];}
Point bpre(){return k[tt-1];}
bool empty(){return ff>tt;}
bool single(){return ff>=tt;}
void pop_front(){++ff;}
void pop_back(){--tt;}
void push(Point x){k[++tt]=x;}
private:
Point k[N];
int ff=1,tt=0;
};
Deque q;
int main(){
int n;
ll tmp=0,sum=0,L;
Point d;
scanf("%d%lld",&n,&L);
for(int i=1;i<=n;i++)scanf("%lld",&tmp),sum+=tmp,A[i]=sum+i,B[i]=sum+i-1-L;
dp[1]=Pow(A[1]-1-L),q.push(mp(2*A[1],dp[1]+Pow(A[1]),1));
for(int i=2,u;i<=n;i++){
while(!q.empty()&&q.back().x==2*A[i])q.pop_back();
while(!q.single()&&S(q.front(),q.fnxt())-(ld)B[i]<1e-7)q.pop_front();
u=q.front().u;
dp[i]=dp[u]+Pow(B[i])-2*B[i]*A[u]+Pow(A[u]),d=mp(2*A[i],dp[i]+Pow(A[i]),i);
while(!q.single()&&S(q.back(),q.bpre())>S(q.back(),d))q.pop_back();
q.push(d);
}
printf("%lld\n",dp[n]);
return 0;
}
过样例,
我们发现还有一个细节,即任意一个
所以要在一开始插一个虚点
每一道题插虚点的方法都不太一样
本题
但是
我们经过思考发现应当是
即表示可以从第一个到第
肯定不能在凭空付一个
所以插一个虚点
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
const int N=50098;
ll dp[N],A[N],B[N];
struct Point{ll x,y;int u;};
//dp[i]=dp[j]+B[i]^2-2B[i]A[j]+A[j]^2=dp[j]-(B[i]-A[j])^2
//dp[j]+A[j]^2=B[i]*2A[j]+(dp[i]-B[i]^2)
//Point(2A[j],dp[j]+A[j]^2)
inline Point mp(ll x,ll y,int u){Point tmp;tmp.x=x,tmp.y=y,tmp.u=u;return tmp;}
inline ld S(Point a,Point b){return (ld)(a.y-b.y)/(ld)(a.x-b.x);}
inline ll Pow(ll x){return x*x;}
class Deque{
public:
Point front(){return k[ff];}
Point fnxt(){return k[ff+1];}
Point back(){return k[tt];}
Point bpre(){return k[tt-1];}
bool empty(){return ff>tt;}
bool single(){return ff>=tt;}
void pop_front(){++ff;}
void pop_back(){--tt;}
void push(Point x){k[++tt]=x;}
private:
Point k[N];
int ff=1,tt=0;
};
Deque q;
int main(){
int n;
ll tmp=0,sum=0,L;
Point d;
scanf("%d%lld",&n,&L);
for(int i=1;i<=n;i++)scanf("%lld",&tmp),sum+=tmp,A[i]=sum+i,B[i]=sum+i-1-L;
q.push(mp(0,0,0));
for(int i=1,u;i<=n;i++){
while(!q.single()&&S(q.front(),q.fnxt())-(ld)B[i]<0)q.pop_front();
u=q.front().u;
dp[i]=dp[u]+Pow(B[i]-A[u]),d=mp(2*A[i],dp[i]+Pow(A[i]),i);
while(!q.empty()&&q.back().x==2*A[i])q.pop_back();
while(!q.single()&&S(q.back(),q.bpre())>S(q.back(),d))q.pop_back();
q.push(d);
}
printf("%lld\n",dp[n]);
return 0;
}
[Example 02 任务安排问题]
题意简述:
机器上有
从时刻
每批任务开始前,机器需要启动时间
同一批任务将在同一时刻完成。
每个任务的费用是它的完成时刻乘以其费用
求最小的总费用。
Basic [ACP3195 任务安排1]
Solution
这题有一个点很不好处理
就是费用还和这个完成时刻有关
我们发现完成时刻肯定会加一个前缀和
但是还有若干个
思考貌似我们可以记录一个
设
然后转移即为
这个转移是
考虑优化(因为一般斜率优化很少有让你优化二维转移方程的)
算了还是看题解吧
这题里面涉及到了一个思想:部分费用提前计算
啥意思呢?
就是我们在求一个
就是因为我们搞了这一波开机
后面的所有任务都会受影响
与其到后面一个一个的加还不如我提前就整好了
所以新版的
这是一个
Extra 01[ACP3195 任务安排2]
Solution
仿照以前的思路
先去掉
要求最小化最后一项,仍然是求下凸壳
点的坐标为
斜率为
仍然满足横坐标单调不降和斜率单调不降
可以直接套板子
虚点还是直接插
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
const int N=300009;
int n,S;
ll dp[N],t[N],c[N];
struct Point{ll x,y;int u;};
inline Point mp(ll x,ll y,int u){Point tmp;tmp.x=x,tmp.y=y,tmp.u=u;return tmp;}
inline ld Slope(Point a,Point b){return (ld)(a.y-b.y)/(ld)(a.x-b.x);}
class Deque{
public:
Point front(){return k[ff];}
Point fnxt(){return k[ff+1];}
Point back(){return k[tt];}
Point bpre(){return k[tt-1];}
bool empty(){return ff>tt;}
bool single(){return ff>=tt;}
void pop_front(){++ff;}
void pop_back(){--tt;}
void push(Point x){k[++tt]=x;}
private:
Point k[N];
int ff=1,tt=0;
}q;
int main(){
int a,b;
Point d;
scanf("%d%d",&n,&S);
for(int i=1;i<=n;i++)scanf("%d%d",&a,&b),t[i]=t[i-1]+a,c[i]=c[i-1]+b;
q.push(mp(0,0,0));
for(int i=1,u;i<=n;i++){
while(!q.single()&&Slope(q.front(),q.fnxt())<t[i])q.pop_front();
u=q.front().u,dp[i]=dp[u]+t[i]*(c[i]-c[u])+S*(c[n]-c[u]),d=mp(c[i],dp[i]-S*c[i],i);
while(!q.empty()&&q.back().x==c[i])q.pop_back();
while(!q.single()&&Slope(q.back(),q.bpre())>Slope(q.back(),d))q.pop_back();
q.push(d);
}
printf("%lld\n",dp[n]);
return 0;
}
Extra 02[ACP3195 任务安排3]
Solution
这题和以前的唯一区别是斜率不一定单调了
然而这个横坐标仍然是单调的
也就意味着单调队列仍然成立
其中的斜率单调递增,也就意味着统计答案可以直接二分
也不需要
所以可以把单调队列换成单调栈
虚点什么的都差不多
开干!
Tips:
- 本题如果
的话只用朴素 double Slope 会被卡
需要手动转乘 - 乘负数 MD 要变号!
我也不知道题解咋写的
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=300007;
struct Point{ll x,y;}k[N];
inline ll cmp(ll a){return (a>0)?1:-1;}
inline ll xx(int a,int b){return k[a].x-k[b].x;}
inline ll yy(int a,int b){return k[a].y-k[b].y;}
ll dp[N],t[N],c[N],S;
int n,st[N],tt=0;
int query(ll val){
int l=1,r=tt-1,mid,ans=tt;
while(l<=r){
mid=(l+r)>>1;
if(yy(st[mid],st[mid+1])*cmp(xx(st[mid],st[mid+1]))>=val*xx(st[mid],st[mid+1])*cmp(xx(st[mid],st[mid+1])))ans=mid,r=mid-1;
else l=mid+1;
}
return st[ans];
}
int main(){
scanf("%d%lld",&n,&S);
for(int i=1;i<=n;i++)scanf("%lld%lld",&t[i],&c[i]),t[i]+=t[i-1],c[i]+=c[i-1];
k[0]={0,0},st[++tt]=0;
for(int i=1,j;i<=n;i++){
j=query(t[i]);
dp[i]=dp[j]+t[i]*c[i]-t[i]*c[j]+S*c[n]-S*c[j];
k[i]={c[i],dp[i]-S*c[i]};
while(tt>0&&k[st[tt]].x==k[i].x&&k[st[tt]].y>=k[i].y)tt--;
while(tt>1&&yy(st[tt],st[tt-1])*xx(st[tt],i)*cmp(xx(st[tt],i)*xx(st[tt],st[tt-1]))>=yy(st[tt],i)*xx(st[tt],st[tt-1])*cmp(xx(st[tt],i)*xx(st[tt],st[tt-1])))tt--;
st[++tt]=i;
}
printf("%lld\n",dp[n]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库