斜率优化入门
本文不讨论CDQ,平衡树维护凸包
斜率优化入门
我们讨论类似于以下的DP转移式(
比较两个决策
得到不等式:
移项变式得到:
令
此时根据
这是
我们这里设
这里写一个引理:我们设一条斜率为
证明:注意到
考虑下面这种情况:
由于我们只讨论
容易发现,此时的情况是先与
再者:
此时先与
通过观察,不难发现,如果直线
所以说
这种上凸的情况可以删去
推而广之,可以发现我们维护的是这样的一个凸包:
而不难发现的是,如果
此时
示例代码:
q[++t]=0;
for(int i=1;i<=n;i++){
while(h<t&&s[i]*down(q[h+1],q[h])>up(q[h+1],q[h]))h++;
get(i,q[h]);
while(h<t&&up(i,q[t])*down(q[t],q[t-1])<up(q[t],q[t-1])*down(i,q[t]))t--;
q[++t]=i;
}
cout<<f[n]<<endl;
}
实现时注意一个点,优先插入一个点
这使得我们引出一个问题:如果
很简单,就是
这时候怎么办?我们只能用单调队列维护凸包的下凸性质,而不能高效查找这个最优决策点,怎么办呢?
回想起之前的引理:
那么,设按照
注意边界的判断。
示例代码:
int find(int i,int k){
if(l==r)return q[l];
int L=l,R=r;
while(L<R){
int mid=(L+R)>>1;
if(down(q[mid+1],q[mid])<=k*up(q[mid+1],q[mid]))L=mid+1;
else R=mid;
}
return q[L];
}
//以下单调队列部分
l=r=1;
for(int i=1;i<=n;i++){
int p=find(i,s+T[i]);
f[i]=f[p]-(s+T[i])*C[p]+T[i]*C[i]+s*C[n];
while(l<r&&down(q[r],q[r-1])*up(i,q[r])>=down(i,q[r])*up(q[r],q[r-1]))r--;
q[++r]=i;
}
例题
T1
考虑一种贪心,我们将每一块土地的左下角都放在坐标系的原点,容易发现,如果存在点
我们需要找到若干个矩形,使得所有点都被覆盖,求矩形面积之和的最小值。将所有点的
设
比较两个决策
设
所以实际需要维护的是一个上凸壳。并且
#include<iostream>
#include<algorithm>
using namespace std;
#define int long long
#define N 500500
#define M 1050050
struct node{
int x,y;
}a[N],b[N];
int h=1,t=1,c[M+500],cnt[N],n,m,tot,f[N],x[N],y[N],q[N];
bool cmp(node a,node b){
return a.x==b.x?a.y<b.y:a.x>b.x;//这里是按y从小到大哦
}
#define lowbit(x) x&-x
void add(int x,int k){
while(x<M){
c[x]+=k;x+=lowbit(x);
}
}
int ask(int x){
int ans=0;
while(x){
ans+=c[x];x-=lowbit(x);
}
return ans;
}
void init(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i].x>>a[i].y;
sort(a+1,a+n+1,cmp);
add(a[1].y,1);
for(int i=2;i<=n;++i){
cnt[i]=i-1-ask(a[i].y-1);//这里起到了去重的作用,不-1会WA#7
add(a[i].y,1);
}
for(int i=1;i<=n;i++)if(!cnt[i])b[++tot]=a[i];
reverse(b+1,b+tot+1);//翻过来
for(int i=1;i<=tot;i++)x[i]=b[i].x,y[i]=b[i].y;
//cout<<endl;
//for(int i=1;i<=tot;i++)cout<<x[i]<<" "<<y[i]<<endl;
}
int X(int x){
return y[x+1];
}
int Y(int s){
return f[s];
}
int up(int i,int j){
return Y(i)-Y(j);
}
int down(int i,int j){
return X(i)-X(j);
}
long double slope(int i,int j){
return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);//必须特判
}
void get(int s,int k){
f[s]=f[k]+x[s]*y[k+1];
}
signed main(){
ios::sync_with_stdio(false);
init();
for(int i=1;i<=tot;i++){
while(h<t&&slope(q[h],q[h+1])>=-1.0*x[i])h++;//记得乘-1
get(i,q[h]);
while(h<t&&slope(q[t],q[t-1])<=slope(i,q[t]))t--;
q[++t]=i;
}
cout<<f[tot];
}
T2
设
容易得知:
对其进行前缀和优化,设
带入并抽离与
比较决策
则
所以设
就可以得到:
维护一个下凸壳,这里由于
注意一个坑点是
inline int X(int x){
return s[x];
}
inline int Y(int x){
return f[x]+t[x];
}
inline int up(int i,int j){
return Y(i)-Y(j);
}
inline int down(int i,int j){
return X(i)-X(j);
}
inline long double slope(int i,int j){
return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);
}
inline void get(int w,int k){
f[w]=f[k]+c[w]-t[w]+t[k]+x[w]*s[w]-x[w]*s[k];
}
signed main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++){
cin>>x[i]>>p[i]>>c[i];
s[i]=s[i-1]+p[i];t[i]=t[i-1]+x[i]*p[i];
}
for(int i=1;i<=n;i++){
while(h<ta&&slope(q[h],q[h+1])<=x[i])h++;
get(i,q[h]);
while(h<ta&&slope(i,q[ta])<=slope(q[ta],q[ta-1]))ta--;
q[++ta]=i;
}
int sit=n;
while(p[sit]==0)sit--;
int ans=f[n];
for(int k=sit;k<=n;k++)ans=min(ans,f[k]);
cout<<ans<<endl;
}
T3
设
显然在最优策略里面,
这个问题等价于:在一段单调不降的序列中,将其分为
对其进行前缀和优化,不妨设
对其内层进行斜率优化,比较决策
则得到:
设
则得到:
显然
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
#define N 100500
#define P 150
int f[P][N],D[N],q[N],Q[N],S[N],A[N],H[N],T[N],n,m,h,t,p;
inline int X(int x,int t){
return x;
}
inline int Y(int x,int i){
return f[i-1][x]+A[x];
}
inline int up(int t,int i,int j){
return Y(i,t)-Y(j,t);
}
inline int down(int t,int i,int j){
return X(i,t)-X(j,t);
}
inline long double slope(int t,int i,int j){
return down(t,i,j)==0?1e18:1.0*up(t,i,j)/down(t,i,j);
}
inline void get(int i,int j,int k){
f[i][j]=f[i-1][k]-A[j]+A[k]+(j-k)*Q[j];
}
void init(){
cin>>n>>m>>p;
for(int i=1;i<=m;i++)f[0][i]=0x3f3f3f3f3f3f;
f[0][0]=0;
for(int i=2;i<=n;i++){
cin>>D[i];
S[i]=D[i]+S[i-1];
}
for(int i=1;i<=m;i++){
cin>>H[i]>>T[i];
Q[i]=T[i]-S[H[i]];
}
sort(Q+1,Q+m+1);
for(int i=1;i<=m;i++){
A[i]=A[i-1]+Q[i];
}
}
void solve(int i){
h=t=1;q[1]=0;
for(int j=1;j<=m;j++){
while(h<t&&slope(i,q[h],q[h+1])<=Q[j])h++;
get(i,j,q[h]);
while(h<t&&slope(i,j,q[t])<=slope(i,q[t],q[t-1]))t--;
q[++t]=j;
}
}
signed main(){
ios::sync_with_stdio(false);
init();
for(int i=1;i<=p;i++)solve(i);
cout<<f[p][m]<<endl;
}
T4
引理:选择的每一段贝壳,左右端点大小相等。
证明:考虑反证法,当这段贝壳的
所以,设
拆开括号得到:
所以比较决策
设
容易得到:
大于号维护上凸包。
注意到求
所以我们需要维护一个单调结构,但只是从最后插入从最后取决策,所以应该使用单调栈进行维护。注意我们是对每一个值都开一个单调栈维护。
Code:
#include<stack>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
#define N 105000
#define ll long long
int t[N],s[N],n,m,pre[N];
ll f[N];
stack<int>q[N];
inline ll X(int x){
return 1ll*s[x]*t[x]-1ll*s[x];
}
inline ll Y(int x){
return 1ll*f[x-1]+1ll*(t[x]-1)*(t[x]-1)*s[x];
}
inline ll up(int i,int j){
return Y(i)-Y(j);
}
inline ll down(int i,int j){
return X(i)-X(j);
}
inline long double slope(int i,int j){
return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);
}
inline void get(int i,int j){
//cout<<i<<" "<<j<<endl;
if(!j){f[i]=s[i];return ;}
f[i]=1ll*f[j-1]+1ll*(t[i]-t[j]+1)*(t[i]-t[j]+1)*s[i];
//cout<<f[j-1]<<" "<<(t[i]-t[j]+1)<<" "<<s[i]<<endl;
}
void read(int &x){
x=0;char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
}
void init(){
read(n);
for(int i=1;i<=n;i++)read(s[i]);
for(int i=1;i<=n;i++)t[i]=t[pre[s[i]]]+1,pre[s[i]]=i;
for(int i=1;i<=n;i++)if(q[s[i]].empty())q[s[i]].push(0);
}
signed main(){
ios::sync_with_stdio(false);
init();
for(int i=1;i<=n;i++){
while(q[s[i]].size()>1){
int x=q[s[i]].top();q[s[i]].pop();
int y=q[s[i]].top();
if(slope(x,y)>slope(i,x)){
q[s[i]].push(x);break;
}
}
q[s[i]].push(i);
while(q[s[i]].size()>1){
int x=q[s[i]].top();q[s[i]].pop();
int y=q[s[i]].top();
if(slope(x,y)>t[i]*2){
q[s[i]].push(x);break;
}
}
get(i,q[s[i]].top());
}
//for(int i=1;i<=n;i++)cout<<f[i]<<" ";
cout<<f[n];
}
T5
板子题,划分区间。
设
按照斜率优化套路,整理式子,提出仅含
稍微整理,设
可以得到:
注意到
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 1006060
#define int long long
int q[N],h=1,t=1,n,m,a,b,c,s[N],f[N];
int X(int x){
return 2*s[x];
}
int Y(int x){
return f[x]+a*s[x]*s[x]-b*s[x];
}
int up(int i,int j){
return Y(i)-Y(j);
}
int down(int i,int j){
return X(i)-X(j);
}
long double slope(int i,int j){
return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);
}
void get(int i,int j){
// cout<<i<<" "<<j<<endl;
f[i]=f[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;
}
void read(int &x){
int s=0,w=1;
char ch=getchar();
while(ch>'9'||'0'>ch){
if(ch=='-')w=-1;
ch=getchar();
}
while(ch>='0'&&'9'>=ch){
s=s*10+ch-'0';
ch=getchar();
}
x=s*w;
}
signed main(){
//freopen("special.in","r",stdin);
//freopen("special.out","w",stdout);
read(n),read(a),read(b),read(c);
for(int i=1;i<=n;i++)f[i]=-0x3f3f3f3f3f3f3f;
for(int i=1;i<=n;i++)read(s[i]);
for(int i=1;i<=n;i++)s[i]+=s[i-1];
for(int i=1;i<=n;i++){
while(h<t&&slope(q[h],q[h+1])>=1.0*a*s[i])h++;
get(i,q[h]);
while(h<t&&slope(q[t],q[t-1])<=slope(i,q[t]))t--;
q[++t]=i;
}
cout<<f[n];
return 0;
}
T6
首先,设
设
所以,分类讨论。
第一个情况,
第二个情况,
二者等价,都为:
所以,设
套路:比较决策
进行移项,变式,设
则
小于号,下凸壳。
需要注意的是,
而在此题中有特殊情况,也即所有的
这里有两种处理方式,第一种是对
#include<bits/stdc++.h>
using namespace std;
#define N 500500
#define int long long
int n,f[N],g[N],q[N],s[N],h=1,t=1,a[N],S;
int Y(int x){
return s[x];
}
int X(int x){
return x;
}
int up(int i,int j){
return Y(i)-Y(j);
}
int down(int i,int j){
return X(i)-X(j);
}
long double slope(int i,int j){
return down(i,j)==0?1e18:1.0*up(i,j)/down(i,j);
}
void get1(int i,int j){
// cout<
f[i]=s[i]-s[j]+(j-i)*a[i];
}
void read(int &x){
int s=0,w=1;
char ch=getchar();
while(ch>'9'||'0'>ch){
if(ch=='-')w=-1;
ch=getchar();
}
while(ch>='0'&&'9'>=ch){
s=s*10+ch-'0';
ch=getchar();
}
x=s*w;
}
inline int find (int val){//二分找斜率
int L=h+1, R=t, cur=1;
while(L<=R){
int mid=(L+R)>>1;
if(slope(q[mid],q[mid-1])<=1.0*val)L=mid+1,cur=mid;//注意这个二分别挂掉了。
else R=mid-1;
}
return q[cur];
}
signed main(){
//freopen("maxweight.in","r",stdin);
//freopen("maxweight.out","w",stdout);
read(n);
for(int i=1;i<=n;i++)read(a[i]);
for(int i=1;i<=n;i++)S+=a[i]*i,s[i]=s[i-1]+a[i];
for(int i=1;i<=n;i++){
while(h<t&&slope(i,q[t])<=slope(q[t],q[t-1]))--t;//下凸壳,斜率递减
q[++t]=i;
}
for(int i=1;i<=n;i++){
get1(i,find(a[i]));
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,f[i]);
cout<<S+ans;
return 0;
}
步骤总结
首先得写出状态转移方程,类似于:
然后比较两个决策
整理后可以得到一个斜率式:
令
此时根据
然后根据实际情况,我们可以分为:
与 都具备单调性,此时使用单调队列维护即可(事实上我们仅需维护凸包的一半),每次取队头作为最优决策 不具备单调性, 仍然具有,此时仍然使用单调队列维护凸包,但最优决策需要二分查找- 如果二者都不单调,需要使用平衡树/CDQ分治动态插入。(较为高深,俺不会)。
- 需要注意的是,根据实际情况,从尾部取决策可能会考虑使用单调栈进行维护
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!