斜率优化dp
斜率优化dp
一种和数学联系很大的\(dp\)的优化方式
做题时的思路一般如下:
如果做\(dp\)题时,这道题转移\(dp[i]\)的值时需要用到前面的决策\(dp[j]\)的值,且出现了i与j的乘积时,考虑斜率优化
做题步骤如下:
一.根据题意,灵活列出一般的不含前缀和优化的\(dp\)方程,然后根据\(dp\)方程的特点,加上前缀和优化(其它的前缀优化的题也可以这么做)
二.将只与\(j\)有关的式子放在等式的左边,将含有\(“i"\)与\(“j"\)的乘积的式子放在等式的右边,将只与i有关的式子放在等式的右边,发现这与平面直角坐标系中直线的斜率式\(y = kx + b\)相似
三.然后找\(x,k\)的单调性:
\(1.\)若\(x\)不单调,考虑平衡树\(/\)\(CDQ\)分治
$ 2.\(若\)x$单调,考虑单调队列:
(1)若k单调,维护单调队列的指针
(2)若k不单调,在单调队列中使用二分查找
四.画出\(y=kx+b\)简图,考虑k的正负,进一步考虑维护上凸包/下凸包
例题
一本通例题三连
例1 任务安排1
题意:有\(n\)个排成一个序列且不能改变顺序的任务需要执行,执行第\(i\)个任务需要时间\(T[i]\),在每批任务完成之前,机器需要\(S\)的启动时间。一个任务完成后需等待该批任务所需时间全部完成。每个任务完成的费用等于它的完成时刻乘以一个费用系数\(C[i]\)。规划一个从费用最小的分组方案。
数据范围:\(1<=n<=5000 1<=S<=50 1<=Ti,Ci<=100\)
法一:复杂度\(O(n^3)\)
设\(f[i][j]\)表示前i批任务完成前j个任务的最小费用
\(f[i][j]=min(f[i][j],f[i-1][k]+(S*i+sumT[j])*(sumC[j]-sumC[k]));\)
法二:复杂度\(O(n^2)\)
设\(f[i]\)表示完成前i个任务的最小费用
\(f[i]=min(f[i],f[j]+(sumC[i]-sumC[j])*sumT[i]+S*(sumC[n]-sumC[j]));\)
我们没有直接求每个任务的完成时刻,而是在一批任务“开始”对后续任务产生影响时,就先把费用累加到结果中。这是一种名为“费用提前计算”的经典思想。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n,s,T[5010],C[5010];
int sumT[5010],sumC[5010];
int dp[5010];
int main()
{
scanf("%d%d",&n,&s);
for(int i=1;i<=n;i++)scanf("%d%d",&T[i],&C[i]);
for(int i=1;i<=n;i++)
{
sumT[i]=sumT[i-1]+T[i];
sumC[i]=sumC[i-1]+C[i];
}
memset(dp,0x3f3f3f3f,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]));//max->min //sumC[n]-sumC[i-1]->sumC[n]-sumC[j-1] //sumC[n]-sumC[j-1],sumC[i]-sumC[j-1]中的j-1改成j
}
}
printf("%d\n",dp[n]);
return 0;
}
例2 任务安排2
题意:与\(“\)任务安排1\("\)相同
数据范围:\(1<=n<=3\times10^5 \,\,\,1<=S,Ti,Ci<=512\)
我们发现时间复杂度为\(O(n^2)\)的算法在这里已经行不通了,我们考虑将复杂度优化为\(O(n)\)或\(O(nlogn)\)
这里我们发现对于上一道题法二的\(dp\)方程中存在\(“sumC[j]\times sumT[i]”\)一项,考虑斜率优化
将式子去掉\(min\),化为\(f[j]=(S+sumT[i])*sumC[j] + f[i] - sumT[i] * sumC[i] - S * sumC[n]\)
$ y = k x + b$
我们维护一个单调队列和下凸包即可。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define maxn 10100
#define ll long long
int n;
ll s;
ll sumT[maxn],sumC[maxn];
ll f[maxn];
int q[maxn];
signed main()
{
scanf("%d%lld",&n,&s);
for(int i=1;i<=n;i++)
{
ll T,C;scanf("%lld%lld",&T,&C);
sumT[i]=sumT[i-1]+T;
sumC[i]=sumC[i-1]+C;
}
int head=1,tail=1;
memset(f,0x3f3f3f3f,sizeof(f));
f[0]=0;
q[1]=0;
for(int i=1;i<=n;i++)
{
while(head<tail&&((f[q[head+1]]-f[q[head]])<=(s+sumT[i])*(sumC[q[head+1]]-sumC[q[head]])))head++;
f[i]=f[q[head]]+(sumC[i]-sumC[q[head]])*sumT[i]+s*(sumC[n]-sumC[q[head]]);
while(head<tail&&((f[i]-f[q[tail]])*(sumC[q[tail]]-sumC[q[tail-1]])<=(f[q[tail]]-f[q[tail-1]])*(sumC[i]-sumC[q[tail]])))tail--;
q[++tail]=i;
}
printf("%lld\n",f[n]);
return 0;
}
例3 任务安排3
题意:与“任务安排1“相同
数据范围:\(1<=n<=3\times10^5 \,\,\,1<=S,Ci<=512 \,\,\,-512<=Ti<=512\)
我们发现这道题中\(T\)可能为负数,所以\(sumT[i]\)可能为负数,故斜率k不再单调递增,我们不能用前面的指针维护单调队列了,但是仍需用单调队列维护下凸包。我们可以在单调队列中二分查找一个点\(p\)使得\(p\)左边的线段斜率小于\(k\),右边的线段斜率大于\(k\)。我们注意到\(k\)可能为负,但是经过分析发现这并不影响答案。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#define maxn 300100
#define ll long long
int n,l,r;
ll s,sumT[maxn],sumC[maxn];
int q[maxn];
ll f[maxn];
int query(ll x,int y)
{
if(l==r)return q[l];
int L=l,R=r,ans=0;
while(L<=R)
{
int mid=(L+R)>>1;
if((f[q[mid]]-f[q[mid-1]])<=x*(sumC[q[mid]]-sumC[q[mid-1]]))
{
ans=mid;L=mid+1;
}
else R=mid-1;
}
return q[ans];
}
int main()
{
scanf("%d%lld",&n,&s);
for(int i=1;i<=n;i++)
{
ll T,C;scanf("%lld%lld",&T,&C);
sumT[i]=sumT[i-1]+T;
sumC[i]=sumC[i-1]+C;
}
l=1,r=1;
q[1]=0;
memset(f,0x3f3f3f3f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
{
int p=query(s+sumT[i],i);
f[i]=f[p]+(sumC[i]-sumC[p])*sumT[i]+s*(sumC[n]-sumC[p]);
while(l<r&&((f[i]-f[q[r]])*(sumC[q[r]]-sumC[q[r-1]])<=(f[q[r]]-f[q[r-1]])*(sumC[i]-sumC[q[r]])))r--;
q[++r]=i;
}
printf("%lld\n",f[n]);
return 0;
}
玩具装箱
题意:有\(n\)件玩具,需要按顺序分批压缩,第\(i\)件玩具压缩后长度变为\(C[i]\),如果将第i件到第j件玩具压缩在一起,容器的长度为\(x=j−i+C_i+C_{i+1}+...+C_j\),长度为x的容器的制作费用为\((x-L)^2\),容器长度任意大,求最小花费。
\(1<=n<=10^5\,\,\, 1<=L,C[i]<=10^7\)
写出\(dp\)方程后发现有\(“i"\)和\(“j”\)的乘积,考虑斜率优化,时间复杂度:\(O(n)\)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define maxn 50100
#define ll long long
int n,L;
int q[maxn];
ll sum[maxn],dp[maxn];
ll a(int x){return sum[x]+x;}
ll b(int x){return sum[x]+x+L+1;}
ll X(int x){return b(x);}
ll Y(int x){return dp[x]+b(x)*b(x);}
int main()
{
scanf("%d%d",&n,&L);
for(int i=1;i<=n;i++)
{
int C;scanf("%d",&C);
sum[i]=sum[i-1]+C;
}
int l=1,r=1;
q[1]=0;
memset(dp,0x3f3f3f3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++)
{
while(l<r&&((Y(q[l+1])-Y(q[l]))<=2*a(i)*(X(q[l+1])-X(q[l]))))l++;
dp[i]=dp[q[l]]+(sum[i]-sum[q[l]]+i-q[l]-1-L)*(sum[i]-sum[q[l]]+i-q[l]-1-L);
while(l<r&&((Y(i)-Y(q[r]))*(X(q[r])-X(q[r-1]))<=(Y(q[r])-Y(q[r-1]))*(X(i)-X(q[r]))))r--;
q[++r]=i;
}
printf("%lld\n",dp[n]);
return 0;
}
Cats Transport
题意:有\(m\)只猫,\(p\)个铲屎官,\(n\)个山丘,山丘\(i\)与山丘\(i-1\)的距离是\(D[i]\)米,铲屎官们住在\(1\)号山丘,第i只猫去山丘\(H[i]\)玩耍\(T[i]\)个单位时间,铲屎官从山丘\(1\)走到山丘\(n\),移动速度为\(1\)米每单位时间,能带上任意数量的猫子。求猫子们最小的等待时间之和。
\(2<=n<=10^5 1<=m<=10^5 1<=P<=100\)
设\(A[i]\)表示铲屎官至少在\(A[i]\)时刻出发能接到第i只猫子
显然铲屎官会接到\(A\)值小于铲屎官出发时间的连续几只猫子,这就转化为了类似于“任务安排”的问题
设\(f[i][j]\)表示第\(i\)个铲屎官接到前\(j\)只猫子的最小等待时间,注意当\(i\)为1时要特殊处理。然后就是标准的斜率优化啦。
时间复杂度:\(O(PM)\)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100100
#define ll long long
int n,m,p;
int D[maxn],H[maxn],T[maxn];
ll A[maxn];
ll S[maxn];
int q[maxn],cnt=0;
ll f[110][maxn];
ll X(int i) { return i; }
ll Y(int j) { return f[cnt][j] + S[j]; }//
ll mymin(ll x, ll y){ if(x < y) return x; else return y; }
int main()
{
scanf("%d%d%d", &n, &m, &p);
for(int i = 2; i <= n; i++)
{
scanf("%d", &D[i]);
D[i] += D[i - 1];
}
for(int i = 1; i <= m; i++)scanf("%d%d", &H[i], &T[i]);
for(int i = 1; i <= m; i++)
{
A[i] = T[i] - D[H[i]];
}
// for(int i = 1;i <= n; i++)printf("D[%d] = %d\n", i, D[i]);
// for(int i = 1;i <= m; i++)printf("H[%d] = %d T[%d] = %d A[%d] =%d\n", i, H[i], i, T[i], i, A[i]);
sort(A + 1, A + m + 1);
for(int i = 1; i <= m; i++)S[i] = S[i - 1] + A[i];
memset(f, 0x3f, sizeof(f)); f[0][0] = 0;
for(int i = 1; i <= p; i++)
{
int l = 1, r = 1; cnt = i - 1; q[1] = 0;
for(int j = 1; j <= m; j++)
{
while(l < r && (Y(q[l + 1]) - Y(q[l])) <= A[j] * (X(q[l + 1]) - X(q[l])))l++;
f[i][j] = f[i - 1][q[l]] + A[j] * (j - q[l]) -(S[j] - S[q[l]]);
while(l < r && (Y(j) - Y(q[r])) * (X(q[r]) - X(q[r-1])) <= (Y(q[r]) - Y(q[r - 1])) * (X(j) - X(q[r])))r--;//////////////////////
q[++r] = j;
}
}
printf("%lld\n", f[p][m]);
return 0;
}
仓库建设
题意:有\(n\)个工厂,由高到低分布在一座山上,工厂\(1\)在山顶,工厂\(n\)在山脚,每个工厂的产品需要存在仓库里,第\(i\)个工厂有\(P[i]\)件成品,在第\(i\)个工厂位置建设仓库的费用为\(C[i]\),产品只能往山下运,已知工厂\(i\)到工厂\(1\)的距离为\(X[i]\),求最小的总费用。
\(1<=n<=1000000\)
列出\(dp\)方程后发现\(“i"\)与\(”j"\)的乘积,标准的斜率优化,注意这道题中用到的前缀和优化的技巧
#include <iostream>
#include <cstdio>
using namespace std;
#define maxn 1000100
#define ll long long
int n;
ll x[maxn];
int p[maxn], c[maxn];
ll spx[maxn], sp[maxn];
int q[maxn];
ll f[maxn];
ll X(int i) { return sp[i]; }
ll Y(int i) { return f[i] + spx[i]; }
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
{
scanf("%d%d%d", &x[i], &p[i], &c[i]);
sp[i] = sp[i - 1] + p[i];
spx[i] = spx[i - 1] + p[i] * x[i];
}
int l = 1, r = 1;
q[1] = 0;
for(int i = 1; i <= n; i++)
{
while(l < r && (Y(q[l + 1]) - Y(q[l])) <= x[i] * (X(q[l + 1]) - X(q[l])))l++;
f[i] = f[q[l]] + (sp[i] - sp[q[l]]) * x[i] - (spx[i] - spx[q[l]]) + c[i];
while(l < r && (Y(i) - Y(q[r])) * (X(q[r]) - X(q[r - 1])) <= (Y(q[r]) - Y(q[r - 1])) * (X(i) - X(q[r])))r--;
q[++r] = i;
}
printf("%lld\n", f[n]);
return 0;
}
特别行动队
题意:有\(n\)个士兵组成的部队,把他们按顺序拆分成若干特别行动队调入战场,已知第\(i\)个士兵的初始战斗力为
\(x[i]\),一支特别行动队的初始战斗力\(x\)为队内士兵初始战斗力之和。初始战斗力\(x\)将按照以下公式进行修正:
\(x=ax^2+bx+c\),求一种编队方式使得所有特别行动队修正后的战斗力之和最大。求出这个最大和。
\(1<=n<=10^6 \,\, -5<=a<=-1\,\,\,\, |b|\,\,|c|<=10^7\,\,\, 1<=x[i]<=100\)
列出\(dp\)方程并转化后发现\(k\)为负数,单调递减,\(x\)为正数,单调递增,故用单调队列维护上凸包
#include <iostream>
#include <cstdio>
using namespace std;
#define ll long long
#define maxn 1000100
int n;
ll a, b, c;
ll x[maxn]={};
int q[maxn];
ll f[maxn]={};
ll X(int i) { return x[i]; }
ll Y(int i) { return f[i] + a * x[i] * x[i] - b * x[i]; }
int main()
{
scanf("%d", &n);
scanf("%lld%lld%lld", &a, &b, &c);
for(int i = 1; i <= n; i++)
{
scanf("%lld", &x[i]);
x[i] += x[i - 1];
}
int l = 1, r = 1;
q[1] = 0;
for(int i = 1; i <= n; i++)
{
while( l < r && (Y(q[l + 1]) - Y(q[l])) >= 2 * a * x[i] * (X(q[l + 1]) - X(q[l])) )l++;
f[i]=Y(q[l]) - 2 * a * x[i] * X(q[l]) + a * x[i] * x[i] + b * x[i] +c;
// f[i]=f[q[l]] + a * (x[q[l]] - x[i]) * (x[q[l]] - x[i]) - b * (x[q[l]] - x[i]) + c;
while( l < r && (Y(i) - Y(q[r])) * (X(q[r]) - X(q[r - 1])) >= (Y(q[r]) - Y(q[r - 1])) * (X(i) - X(q[r])) )r--;
q[++r]=i;
}
printf("%lld\n", f[n]);
return 0;
}
锯木厂选址
题意:与“仓库建设”大致相同,但只能建设两个仓库且建设仓库不需费用,且第\(n\)个工厂一定为仓库
做法为跑两个\(dp\)值\(f\),\(g\),\(f[i]\)表示将第\(1\)个仓库修在i位置的最小运输费用
然后\(g[i]\)从\(f[i]\)利用斜率优化转移即可。
#include <iostream>
#include <cstdio>
using namespace std;
#define maxn 20100
#define ll long long
int n;
ll sd[maxn], sp[maxn], sdp[maxn];
int q[maxn];
ll f[maxn], g[maxn];
ll X(int i) { return sp[i]; }
ll Y(int i) { return f[i] + sdp[i]; }
int main()
{
scanf("%d", &n); n += 1;
for(int i = 1; i <= n; i++)
{
int w, d;
if(i != n)scanf("%d%d", &w, &d);
sd[i + 1] = sd[i] + d;
sp[i] = sp[i - 1] + w;
sdp[i] = sdp[i - 1] + sd[i] * w;
}
for(int i = 1; i <= n; i++)
{
f[i] = sd[i] * sp[i - 1] - sdp[i - 1];
}
long long ans = 1e9; ans *= 1e9;
int l = 1, r = 1; q[1] = 0;
for(int i = 1; i <= n; i++)
{
while(l < r && (Y(q[l + 1]) - Y(q[l])) <= sd[i] * (X(q[l + 1]) - X(q[l])))l++;
g[i] = f[q[l]] + sd[i] * (sp[i] - sp[q[l]]) - (sdp[i] - sdp[q[l]]);
while(l < r && (Y(i) - Y(q[r])) * (X(q[r]) - X(q[r - 1]))<= (Y(q[r]) - Y(q[r - 1])) * (X(i) - X(q[r])))r--;
q[++r] = i;
// printf("j = %d g[%d] = %lld ans = %lld\n", q[l], i, g[i], g[i] + sd[n] * (sp[n - 1] - sp[i - 1]) - (sdp[n - 1] - sdp[i - 1]));
ans = min(ans, g[i] + sd[n] * (sp[n - 1] - sp[i]) - (sdp[n - 1] - sdp[i]));
}
printf("%lld\n", ans);
return 0;
}
征途
题意:从S地到T第有n段路,把n段路分成m天走,使得每一天走过的路的方差最小。 设方差是v,可以证明v*m2是一个正整数,求最小方差 \(\times m^2\)
\(1<=n<=3000\)
这道题我一开始的做法是直接用\(double\)和斜率优化爆算,只得了\(50\)分
发现因为算的次数很多且每次都用了平方,导致\(double\)的精度不够用。
然后看了题解,题目中说的“可以证明\(v\times m^2\)是一个正整数”,我并没有去用,而是直接用答案乘了一个m2,而正解应该是列出答案后化简发现只需利用斜率优化求出所有天的路程的平方和 * m,再减去总路程的平方即可,这道题告诉我们做题时应该运用到题目中给出的每一个条件,没有任何一个条件是无用的。
#include <iostream>
#include <cstdio>
using namespace std;
int n, m;
int d[3100], cnt = 0;
int q[3100], sx;
int dp[3100][3100];
int X(int i) { return d[i]; }
int Y(int i) { return dp[cnt][i] + d[i] * d[i] + 2 * sx * d[i]; }
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
scanf("%d", &d[i]);
d[i] += d[i - 1];
}
sx = 0;
for(int i = 1; i <= n; i++)dp[1][i] = (d[i] - sx) * (d[i] - sx);
for(int i = 2; i <= m; i++)
{
int l = 1, r = 1;
q[1] = 0;
cnt = i - 1;
for(int j = 1; j <= n; j++)
{
while(l < r && (Y(q[l + 1]) - Y(q[l])) <= 2 * d[j] * (X(q[l + 1]) - X(q[l])))l++;
dp[i][j] = dp[i - 1][q[l]] + (d[j] - d[q[l]] - sx) * (d[j] - d[q[l]] - sx);
while(l < r && (Y(j) - Y(q[r])) * (X(q[r]) - X(q[r - 1]))<= (Y(q[r]) - Y(q[r - 1])) * (X(j) - X(q[r])))r--;
q[++r] =j;
}
// printf("i = %d l = %d r = %d\n", i, l, r);
}
//for(int i = 1; i <= n; i++)printf("d[%d] = %d\n", i, d[i]);
/* printf("sx = %d\n", sx);
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= n; j++)printf("f[%d][%d] = %d\n",i,j,dp[i][j]);
}*/
printf("%d\n",dp[m][n] * m - d[n] * d[n]);
return 0;
}