斜率优化入门
本文不讨论CDQ,平衡树维护凸包
斜率优化入门
我们讨论类似于以下的DP转移式(\(\max\)同理)
\(f_i=\min \lbrace f_j+g(i)+h(j)+a(i)b(j)\rbrace\)
比较两个决策\(j,k\),设\(j>k\)且优于\(k\)。
得到不等式:
移项变式得到:\(f_j+h(j)-(f_k+h(k))<-a(i)(b(j)-b(k))\)
令\(Y(n)=f_n+h(n),X(n)=b(n)\)
此时根据\(X(j)-X(k)\)的正负性进行变式,这里设\(X\)单调递增,变式可以得到:
这是\(j\)优于\(k\)的条件。类似地,反着来可以得到:\(\frac{Y(j)-Y(k)}{X(j)-X(k)}\ge -a(i)\)的时候\(k\)优于\(j\)
我们这里设\(K(n)=\frac{Y(n)}{X(n)}\),我们目前只讨论\(-a(i)\)递增,\(X\)递增的情况。
这里写一个引理:我们设一条斜率为\(-a(i)\)的直线,不断向上平移,第一个遇到的点就是最优决策点。
证明:注意到\(Y(i)=f_i+h(i)\),所以最小化\(Y\)就可以最小化\(f\)(\(h\)是常数),故我们实际上需要考虑的就是最小化直线截距。而回到之前的条件:$$\frac{Y(j)-Y(k)}{X(j)-X(k)}<-a(i)$$表示\(j\)优于\(k\),反之\(k\)优于\(j\)。我们设当前平面上插入的点集是\(S\),则实际上对于最优决策点\(k\)而言,所有\(X\)坐标小于\(X(k)\)的点与点\(k\)所在的直线斜率小于\(-a(i)\),右边的都大于等于\(-a(i)\)(等于的原因是此时两个点都最优)。而我们向上平移时遇到的第一个点就满足这个性质且截距最小(如果不能理解随便画几组就能理解了)
考虑下面这种情况:
由于我们只讨论\(-a(i)\)递增的情况,不妨画几个直线看看规律:
容易发现,此时的情况是先与\(C\)相交
再者:
此时先与\(A\)相交更优
通过观察,不难发现,如果直线\(l\)满足\(k_l<k_{AC}\),则与\(C\)相交,若\(k_l>K_{AC}\)则与\(A\)相交,等于时\(A,C\)等价。总之,不可能与\(B\)相交。
所以说\(B\)是无用的决策点,也即
这种上凸的情况可以删去\(B\),仅保留\(AC\)即可。
推而广之,可以发现我们维护的是这样的一个凸包:
而不难发现的是,如果\(-a(i),X\)都是单调的,真正的最优决策点会呈现出这个样子:
此时\(A,B,C,D,E\)都有可能成为最优决策,而\(F,G\)是无用的,可以大胆删去。由此我们可以使用单调队列维护这个凸包的一半,每次取队头即为最优决策
示例代码:
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;
}
实现时注意一个点,优先插入一个点\((0,0)\),只有单调队列里有超过两个元素的时候才能够进行比较决策。
这使得我们引出一个问题:如果\(-a(i)\)不单调呢?
很简单,就是\(F,G\)也可能成为最优决策。
这时候怎么办?我们只能用单调队列维护凸包的下凸性质,而不能高效查找这个最优决策点,怎么办呢?
回想起之前的引理:
\(\frac{Y(j)-Y(k)}{X(j)-X(k)}<-a(i)\) 这是\(j\)优于\(k\)的条件。类似地,反着来可以得到:\(\frac{Y(j)-Y(k)}{X(j)-X(k)}\ge -a(i)\)的时候\(k\)优于\(j\)
那么,设按照\(x\)坐标从小到大,凸包点集为\(S\),由于单调队列里下凸包斜率有序,可以二分查找使得\((S_k,S_{k-1})_k<-a(i)\le (S_k,S_{k+1})\)的点\(S_k\)
注意边界的判断。
示例代码:
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
考虑一种贪心,我们将每一块土地的左下角都放在坐标系的原点,容易发现,如果存在点\(A(x_1,y_1),B(x_2,y_2)\),如果\(x_1\le x_2,y_1\le y_2\)则\(A,B\)肯定在同一组,此时\(A\)肯定不会有任何贡献。所以最后需要考虑的点集是这个样子:
我们需要找到若干个矩形,使得所有点都被覆盖,求矩形面积之和的最小值。将所有点的\(x\)从小到大排序,则\(y\)递减
设\(f_i\)表示覆盖前\(i\)个点所需要的最小代价,容易写出转移方程式:
比较两个决策\(k<j<i\),设\(j\)优于\(k\),得到:
设\(X(n)=y_{n+1},Y(n)=f_n\),则容易得到:
所以实际需要维护的是一个上凸壳。并且\(-x_i\)单调递减。
#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
设\(f_i\)表示前\(i\)个工厂中在第\(i\)个工厂建立仓库的最小代价
容易得知:
对其进行前缀和优化,设\(S_i=\sum_{k=1}^ip_i,T_i=\sum_{k=1}^ix_ip_i\)
带入并抽离与\(i\)有关的项,得到:
比较决策\(j,k\),设\(j>k\)且\(j\)优于\(k\)
则
\(j>k\implies S_j>S_k\)
所以设\(Y(n)=f_n+T_n,X(n)=S_n\)
就可以得到:
维护一个下凸壳,这里由于\(X,x_i\)显然具有单调性,所以用单调队列维护即可。
注意一个坑点是\(p_i=0\)的时候可以不修,所以最后还要检查一下。
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
设\(S_i=\sum_{k=1}^iD_i\),则若从时刻0出发,接到第\(i\)只猫的时间就为:\(T_i-S_{H_i}\)。这个式子进行加减某个值\(x\),就可以得到从\(x\)时刻出发,猫所等待的时间了,故设\(Q_i=T_i-S_{H_i}\)。
显然在最优策略里面,\(P\)个饲养员肯定都是各自选择了一只猫,刚好接走这只猫。所以不妨将\(Q_i\)递增排序,此时若选择刚好带走\(Q_k\),那么就可以顺带带走\(Q_1\sim Q_{k-1}\)。因为这些先耍完等着铲屎官了。这里就凸显一个贪心策略:每个饲养员肯定都是选择其中一段全部带走。设一次性带走区间\([l,r]\)的猫,则肯定是从\(-Q_r\)时刻出发,在0时刻恰好接走\(r\)。则总的等待时间为:\(\sum_{i=l}^r(Q_r-Q_i)\)
这个问题等价于:在一段单调不降的序列中,将其分为\(p\)个连续区间,每个区间的代价为此区间的最大值乘以区间长度并减去此区间权值和,要求最小化每个区间的代价和。
对其进行前缀和优化,不妨设\(A_i=\sum_{k=1}^iQ_k\),设\(f_{i,j}\)表示将前\(i\)个数分为\(j\)段的最小代价,则容易得到状态转移方程:
对其内层进行斜率优化,比较决策\(k_1,k_2\),设\(k_1>k_2\)且\(k_1\)优于\(k_2\)。
则得到:
设\(Y(n)=f_{i-1,n}+A_n,X(n)=n\)
则得到:
显然\(Q,X\)都具备单调性,所以使用单调队列维护,每一次\(i\)的变化将队列清空即可。
#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
引理:选择的每一段贝壳,左右端点大小相等。
证明:考虑反证法,当这段贝壳的\(s_0\)选择的是端点之一的话,如果大小不等,可以往回缩到大小相等的一个端点,此时回缩的那段区间可以有更多的贡献。如果不是端点之一的话,可以从两端缩,原贡献不变,且回缩区间有更大贡献。
所以,设\(f_i\)表示前\(i\)个贝壳里分段的最大贡献,预处理\(t_i=\sum_{k=1}^i[s_k=s_i]\),则:
拆开括号得到:
所以比较决策\(j>k\)且\(j\)优于\(k\),容易得到:
设\(Y(n)=f_{n-1}+(t_{n}-1)^2s_n,X(n)=s_n(t_{n}-1)\)
容易得到:
大于号维护上凸包。
注意到求\(\max\)要使得截距最大化,容易发现随着斜率的增加,最优决策点也在不断向左移动,所以我们维护凸包的一半(斜率\(>2t_i\)的部分),取最后一个合法点。类似于下图:
所以我们需要维护一个单调结构,但只是从最后插入从最后取决策,所以应该使用单调栈进行维护。注意我们是对每一个值都开一个单调栈维护。
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
板子题,划分区间。
设\(f_i\)表示前\(i\)个人组队的最大值,容易得到:
按照斜率优化套路,整理式子,提出仅含\(i\)的项,比较决策\(j,k\),得到:
\(f_j+as_j^2-2as_is_j-bs_j>f_k+as_k^2-2as_is_k-bs_k\)
稍微整理,设\(Y(n)=f_n+as_n^2-bs_n,X(n)=2s_j\)
可以得到:
注意到\(as_i\)递减,\(X\)递增,是大于号,使用单调队列维护上凸壳,更新答案即可。
#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
首先,设\(S=\sum_{i=1}^na_i\times i\),我们仅仅需要考虑变化量,并且将其最大化。
设\(s_i=\sum_{k=1}^ia_k\)
所以,分类讨论。
第一个情况,\(i\)移动到\(j\)的后面,且\(j<i\),这时候的贡献变化为:\(-i\times a_i+(j+1)\times a_i+s_{i}-s_{j}-a_i\)
第二个情况,\(i\)移动到\(j\)的后面,且\(j>i\),这时候的贡献变化为:\(-i\times a_i+(j+1)\times a_i-s_j+s_{i}-a_i\)
二者等价,都为:\((j-i)\times a_i+s_i-s_j\)
所以,设\(f_i\)表示将第\(i\)个数进行移动,可以增加的最大权值,可以得到:
套路:比较决策\(j>k\),\(j\)优于\(k\),得到:
进行移项,变式,设\(Y(n)=s_n,X(n)=n\)
则
小于号,下凸壳。
需要注意的是,\(a_i\)不具备单调性,故需要二分处理。
而在此题中有特殊情况,也即所有的\(j\)都可以对\(i\)产生贡献,故我们不妨先将所有的\(j\)插入凸壳里,然后对于每一个\(a_i\)进行处理。
这里有两种处理方式,第一种是对\(a_i\)进行排序,使用单调队列进行维护,第二种是直接进行二分,问题都不大。
#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;
}
步骤总结
首先得写出状态转移方程,类似于:
\(f_i=\min \lbrace f_j+g(i)+h(j)+a(i)b(j)\rbrace\)
然后比较两个决策\(j,k\),设\(j>k\)且优于\(k\),列出不等式(\(\max\)的时候就是\(>\)):
整理后可以得到一个斜率式:
令\(Y(n)=f_n+h(n),X(n)=b(n)\)
此时根据\(X(j)-X(k)\)的正负性进行变式,这里设\(X\)单调递增,变式可以得到:
\(\frac{Y(j)-Y(k)}{X(j)-X(k)}\)与\(-a(i)\)进行比较,此时如果连接的是\(<\),则需要维护下凸壳,\(>\)就是上凸壳
然后根据实际情况,我们可以分为:
- \(-a(i)\)与\(X\)都具备单调性,此时使用单调队列维护即可(事实上我们仅需维护凸包的一半),每次取队头作为最优决策
- \(-a(i)\)不具备单调性,\(X\)仍然具有,此时仍然使用单调队列维护凸包,但最优决策需要二分查找
- 如果二者都不单调,需要使用平衡树/CDQ分治动态插入。(较为高深,俺不会)。
- 需要注意的是,根据实际情况,从尾部取决策可能会考虑使用单调栈进行维护