关于「斜率优化 DP」戳 这里(加了密码,暂时不公开)。
下面都是一些斜率优化的入门题(按套路做,单调队列维护凸包),可以 从后往前看 自己推一推 QwQ。
1. 「HNOI 2008」玩具装箱
Problem:给定一个长度为 (n) 的序列 (a_1,a_2,cdots,a_n) 以及常数 (m)。现要将 (a) 分成若干段,对于一段 ([l,r]),它的代价为 ((r-l+sum_{i=l}^r a_i-m)^2)。求分段的最小代价。
(1leq nleq 5 imes 10^4,1leq m,a_ileq 10^7)。
Solution:令 (f_i) 表示考虑了前 (i) 个数,分成若干段的最小代价。记 (S_k=sum_{i=1}^k a_i)。那么有:
为了方便化简,我们令 (G_i=S_i+i),得:
任取 (j,k) 且满足 (0≤k<j<i),若从 (j) 转移比 (k) 更优,则有(接下来把平方拆开然后化简即可):
化简过程(省略部分步骤)
一般化简大概是将 只 与 (j,k) 有关的移到右侧,与 (i) 有关的移到左侧。
由于 (a_i) 为正整数,所以 (G_j>G_k),可以将 (G_j-G_k) 直接移到右侧,得到:
然后用单调队列维护一个下凸壳转移即可(维护一个斜率递增的凸壳)。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=5e4+5; int n,m,x,s[N],g[N],f[N],q[N]; int get(int x){ return f[x]+g[x]*g[x]; } double slope(int i,int j){ return 1.0*(get(i)-get(j))/(g[i]-g[j]); } int sqr(int x){return x*x;} signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&x),s[i]=s[i-1]+x,g[i]=s[i]+i; int l=0,r=0; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])<=2*(g[i]-m-1)) l++; f[i]=f[q[l]]+sqr(g[i]-g[q[l]]-(m+1)); while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } printf("%lld ",f[n]); return 0; }
2. 「CEOI 2004」锯木厂选址
Problem:从山顶上到山底下沿着一条直线种植了 (n) 棵老树。第 (i) 棵树的重量为 (w_i),第 (i) 棵树和第 (i+1) 棵树之间的距离为 (d_i)。现在要将它们砍下来运送到锯木厂。
木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。假定运输每公斤木材每米需要一分钱。
求最小运输费用。(2leq nleq 2 imes 10^4,1leq w_ileq 10^4,0leq d_ileq 10^4)。
Solution:
令 (f_i) 为以 (i) 作为第二个锯木厂的最小花费。记 (S_i) 为 (w_i) 的前缀和,(tot) 为 (w_i) 的总和(也就是将所有树全部运送到山脚下的费用),(dis_i) 为 (d_i) 的后缀和。即,(S_i=sum_{j=1}^i w_j,dis_i=sum_{j=i}^n d_j)。则有:
一些解释
相当于是枚举第一个锯木厂的位置 (j)。那么在 (i,j\,(j<i)) 处修了锯木厂的花费为:将 (tot) 减去 从 (j) 厂运送到山脚的额外花费 (S_j imes dis_j)((j) 处修建了锯木厂,那么 ([1,j]) 的树只需运送到 (j),不用运送到山脚下),再减去 从 (i) 厂运送到山脚的额外花费 ((S_i-S_j) imes dis_i)((j<i),(i) 处修了锯木厂,那么 ((j,i]) 的树只需运送到 (i))。
然后这显然是可以斜率优化的,按套路来就可以了。
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
化简过程
(w_i) 为正整数,所以 (S_j>S_k),可以将 (S_j-S_k) 直接移到左侧:
由于 (dis_i) 是递减的,我们可以维护一个上凸壳转移(维护一个斜率递减的凸壳)。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e4+5; int n,w[N],d[N],dis[N],s[N],tot,q[N]; int get(int x){ return s[x]*dis[x]; } double slope(int i,int j){ return 1.0*(get(i)-get(j))/(s[i]-s[j]); } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++) scanf("%lld%lld",&w[i],&d[i]); for(int i=n;i>=1;i--) dis[i]=dis[i+1]+d[i]; for(int i=1;i<=n;i++) s[i]=s[i-1]+w[i],tot+=w[i]*dis[i]; int l=0,r=0,ans=1e18; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])>=dis[i]) l++; ans=min(ans,tot-s[q[l]]*dis[q[l]]-(s[i]-s[q[l]])*dis[i]); while(l<r&&slope(q[r-1],q[r])<=slope(q[r],i)) r--; q[++r]=i; } printf("%lld ",ans); return 0; }
3. 「ZJOI 2007」仓库建设
Problem:略。
Solution:与上一题类似。令 (f_i) 表示在 (i) 位置建设了仓库的最小代价。记 (S_i=sum_{j=1}^i p_j,G_i=sum_{j=1}^i x_jp_j)。
一些解释
枚举上一个仓库的位置。那么只需将 ((j,i]) 的产品运送到 (i),无需再运到山脚。
然后前缀和优化。
然后根据套路做。当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
化简过程
显然有 (S_j>S_k),可将 (S_j-S_k) 移到右侧:
#define int long long using namespace std; const int N=1e6+5; int n,x[N],p[N],c[N],s[N],g[N],f[N],q[N]; int get(int x){ return f[x]+g[x]; } double slope(int i,int j){ return 1.0*(get(i)-get(j))/(s[i]-s[j]); } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++){ scanf("%lld%lld%lld",&x[i],&p[i],&c[i]); s[i]=s[i-1]+p[i],g[i]=g[i-1]+x[i]*p[i]; } int l=0,r=0; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])<=x[i]) l++; f[i]=f[q[l]]+x[i]*(s[i]-s[q[l]])-(g[i]-g[q[l]])+c[i]; while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } printf("%lld ",f[n]); return 0; }
4. 「BZOJ 1597」土地购买
Problem:有 (n) 块土地,第 (i) 块土地长为 (x_i)、宽为 (y_i)。现要将这些土地划分为若干组(每块土地都应该属于且仅属于其中一组。也可以一块土地单独一组),一组土地的代价为这些土地中最大的长乘以最大的宽,即 (max{x_i} imes max{y_i})。求划分的最小代价之和。
(1leq nleq 5 imes 10^4,1leq x_i,y_ileq 10^6)。
Solution:首先,对于土地 (i,j),若 (x_igeq x_j) 且 (y_igeq y_j),则土地 (j) 显然是无用的(可以将 (j) 和 (i) 分为一组,(j) 没有贡献)。
考虑将所有土地按 (x_i) 降序为第一优先级,(y_i) 升序为第二优先级。此时对于连续的一段土地 ([l,r]),(max{x_i}) 一定为 (x_l),但 (max{y_i}) 不确定。考虑通过剔除无用元素使得第二位也存在单调性,使得 (max{y_i}) 一定为 (y_r)。具体见代码。
此时对于划分出的一段 ([l,r]),其代价为 (x_lcdot y_r)。令 (f_i) 表示按此顺序划分前 (i) 块土地的最小代价。转移时,枚举上一组土地的结尾。
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
化简过程
由于 (x_i) 是降序排序的,所以 (x_{j+1}leq x_{k+1}),(x_{j+1}-x_{k+1}leq 0)。将 (x_{j+1}-x_{k+1}) 移到左侧时要变号:
维护下凸壳转移即可。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=5e4+5; int n,m,f[N],q[N],lst; bool vis[N]; struct data{ int x,y; }a[N]; bool cmp(data x,data y){ return x.x!=y.x?x.x>y.x:x.y<y.y; } double slope(int i,int j){ return 1.0*(f[j]-f[i])/(a[i+1].x-a[j+1].x); } signed main(){ scanf("%lld",&n); for(int i=1;i<=n;i++) scanf("%lld%lld",&a[i].x,&a[i].y); sort(a+1,a+1+n,cmp); for(int i=1;i<=n;i++){ //去除无贡献元素 if(i!=1&&a[i].x<=a[lst].x&&a[i].y<=a[lst].y) vis[i]=1; else lst=i; } for(int i=1;i<=n;i++) if(!vis[i]) a[++m]=a[i]; int l=0,r=0; for(int i=1;i<=m;i++){ while(l<r&&slope(q[l],q[l+1])<=a[i].y) l++; f[i]=f[q[l]]+a[q[l]+1].x*a[i].y; while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } printf("%lld ",f[m]); return 0; }
5. 「APIO 2010」特别行动队
Problem:有一支 (n) 名士兵的部队,士兵从 (1) 到 (n) 编号,编号为 (i) 的士兵的初始战斗力为 (x_i)。现要将他们拆分成若干组,同一组中队员的编号应该连续,即为形如 ((i,i+1,cdots,i+k)) 的序列。
对于一个组,它的初始战斗力 (X) 为组内士兵初始战斗力之和,即 (X=x_i+x_{i+1}+cdots+x_{i+k})。它的修正战斗力为 (X'=aX^2+bX+cX),其中 (a,b,c) 是已知的系数((a<0))。
求每组修正战斗力之和的最大值。
(1leq nleq 10^6,-5leq aleq -1,-10^7leq b,cleq 10^7,1leq x_ileq 100)。
Solution:令 (f_i) 表示前 (i) 个人的战斗力之和的最大值。记 (S_i=sum_{j=1}^i x_j)。
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
化简过程
因为 (x_i) 为正整数,所以 (S_j>S_k),将 (S_j-S_k) 移到右侧得:
不等式左侧是单调递减的(题目中保证 (a<0),且显然 (S_i) 递增),右侧分母上的前缀和是单调递增的。
维护上凸壳转移即可(斜率递减),每次的最优决策就是队首。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e6+5; int n,a,b,c,x[N],s[N],q[N],f[N]; int sqr(int x){return x*x;} int get(int x){ return f[x]+a*sqr(s[x])-b*s[x]; } double slope(int i,int j){ return 1.0*(get(i)-get(j))/(s[i]-s[j]); } signed main(){ scanf("%lld%lld%lld%lld",&n,&a,&b,&c); for(int i=1;i<=n;i++) scanf("%lld",&x[i]),s[i]=s[i-1]+x[i]; int l=0,r=0; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])>=2*a*s[i]) l++; f[i]=f[q[l]]+a*sqr(s[i]-s[q[l]])+b*(s[i]-s[q[l]])+c; while(l<r&&slope(q[r-1],q[r])<=slope(q[r],i)) r--; q[++r]=i; } printf("%lld ",f[n]); return 0; }
6. 「APIO 2014」序列分割
Problem:给出一个长度为 (n) 的非负整数序列 ({a_n})。先要将序列分为 (k+1) 个非空的块。为了得到 (k+1) 块,你需要重复下面的操作 (k) 次:
- 选择一个有超过一个元素的块(初始时你只有一块,即整个序列);
- 选择两个相邻元素把这个块从中间分开,得到两个非空的块。
每次操作后将获得那两个新产生的块的元素和的乘积的分数。最大化最后的总得分,要求输出方案。
(2leq nleq 10^5,1leq kleq min(n-1,200),0leq a_ileq 10^4)。
Solution:
我们首先证明答案和分割顺序无关。
如果我们有长度为 (3) 的序列 (x,y,z) 将其分为 (3) 部分,有如下两种分割方法:
- 先在 (x) 后面分割,答案为 (x(y+z)+yz) 即为 (xy+yz+zx)。
- 先在 (y) 后面分割,答案为 ((x+y)z+xy) 即为 (xy+yz+zx)。
然后这个结论可以扩展到任意长度的序列(分析一下贡献),证毕。
令 (F_{i,j}) 表示前 (i) 个数进行 (j) 次分割的最大得分。记 (S_i) 为 (a_i) 的前缀和。
为了方便表述,记 (F_{i,k}) 为 (f_i),(F_{j,k-1}) 为 (g_j),相当于把 (F) 的第二维滚动掉了。
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
化简过程
显然 (S_jgeq S_k),所以 (S_k-S_jleq 0),将 (S_k-S_j) 移到右边需变号:
维护下凸壳转移即可。然后输出方案的话记一个 (pre) 表示从哪里转移过来。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5,M=210; int n,k,a[N],s[N],g[N],f[N],pre[N][M],q[N],x; int get(int x){ return g[x]-s[x]*s[x]; } double slope(int i,int j){ if(s[i]==s[j]) return -1e18; //注意此题中,a[i] 为非负整数,可能会取到 0,所以 s[k]-s[j] 的值可能为 0。这种情况需特判,slope 需返回 -inf(否则算斜率的时候除以 0 就挂了)。 return 1.0*(get(i)-get(j))/(s[j]-s[i]); } signed main(){ scanf("%lld%lld",&n,&k); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),s[i]=s[i-1]+a[i]; for(int j=1;j<=k;j++){ int l=0,r=0; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])<=s[i]) l++; f[i]=g[q[l]]+s[q[l]]*(s[i]-s[q[l]]),pre[i][j]=q[l]; while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } memcpy(g,f,sizeof(f)); } printf("%lld ",f[n]),x=n; for(int i=k;i>=1;i--) x=pre[x][i],printf("%lld%c",x,i==1?' ':' '); return 0; }
7. 「SDOI 2016」征途
Problem:给出一个有 (n) 个数的序列 ({a_n}),现要把其分为 (m) 段,设每段的权值为该段 (a_i) 之和,最小化这 (m) 段的方差 (v),输出 (v imes m^2)。
(1leq nleq 3000,sum a_ileq 30000)。
Solution:与上一题类似。
设 (b_i) 为每段的权值,(overline b) 为平均数,我们要最小化:
将平方拆开,得到:
继续化简,并代入 (overline b=frac{sum_{i=1}^mb_i}{m}),得到:
化简过程
发现后面那部分的 (left(sum_{i=1}^mb_i ight)^2) 等于 (left(sum_{i=1}^n a_i ight)^2) 为定值,我们现在要最小化 (sum_{i=1}^m b_i^2)。
令 (F_{i,j}) 表示前 (i) 个数分为 (j) 段的最小值。记 (S_i) 为 (a_i) 的前缀和。
将 (F) 的第二维用滚动数组滚掉。记 (F_{i,k}) 为 (f_i),(F_{j,k-1}) 为 (g_j)。
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
维护下凸壳转移即可。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=3e4+5; int n,m,x,s[N],f[N],g[N],q[N]; int sqr(int x){return x*x;} int get(int x){ return g[x]+sqr(s[x]); } double slope(int i,int j){ return 1.0*(get(i)-get(j))/(s[i]-s[j]); } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=n;i++) scanf("%lld",&x),s[i]=s[i-1]+x,g[i]=s[i]*s[i]; for(int j=2;j<=m;j++){ int l=0,r=0; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])<=2*s[i]) l++; f[i]=g[q[l]]+sqr(s[i]-s[q[l]]); while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } memcpy(g,f,sizeof(f)); } printf("%lld ",m*f[n]-sqr(s[n])); return 0; }
8. 「Codeforces 311B」Cats Transport
「Codeforces 311B」Cats Transport
Problem:小 S 是农场主,他养了 (m) 只猫,雇了 (p) 位饲养员。农场中有一条笔直的路,路边有 (n) 座山,从 (1) 到 (n) 编号。第 (i) 座山与第 (i?1) 座山之间的距离是 (d_i)。饲养员都住在 (1) 号山上。
有一天,猫出去玩。第 (i) 只猫去 (h_i) 号山玩,玩到时刻 (t_i) 停止,然后在原地等饲养员来接。饲养员们必须回收所有的猫。每个饲养员沿着路从 (1) 号山走到 (n) 号山,把各座山上已经在等待的猫全部接走。饲养员在路上行走需要时间,速度为 (1) 米每单位时间。饲养员在每座山上接猫的时间可以忽略,可以携带的猫的数量为无穷大。
你的任务是规划每个饲养员从 (1) 号山出发的时间,使得所有猫等待时间的总和尽量小。饲养员出发的时间可以为负。
(2leq nleq 10^5,1leq mleq10^5,1leq pleq 100)。
Solution:设 (a_i=t_i-sum_{j=2}^{h_i}d_j),也就是让第 (i) 只猫不等待的出发时间。如果有人从 (T) 时刻出发,那么等待时间为 (T-a_i)。
考虑将 (a_i) 从小到大排序,那么每一个饲养员应该会带走一段连续区间的猫。
令 (F_{i,k}) 表示 (k) 个饲养员带走前 (i) 只小猫的最少等待时间。记 (S_i) 为 (a_i) 的前缀和。
即,第 (k) 个饲养员带走 ((j,i]) 的小猫,那么就在 (a_i) 出发,等待时间为 (a_i(i-1)-(S_i-S_j))。
用滚动数组将 (F) 的第二维滚掉。设 (f_i=F_{i,k},g_j=F_{j,k-1})。则:
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
化简过程
维护下凸壳转移即可。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e5+5; int n,m,p,d[N],a[N],h[N],t[N],s[N],f[N],g[N],q[N]; int get(int x){ return g[x]+s[x]; } double slope(int i,int j){ return 1.0*(get(i)-get(j))/(i-j); } signed main(){ scanf("%lld%lld%lld",&n,&m,&p); for(int i=2;i<=n;i++) scanf("%lld",&d[i]),d[i]+=d[i-1]; for(int i=1;i<=m;i++) scanf("%lld%lld",&h[i],&t[i]),a[i]=t[i]-d[h[i]]; sort(a+1,a+1+m); for(int i=1;i<=m;i++) s[i]=s[i-1]+a[i]; memset(g,0x3f,sizeof(f)),g[0]=0; for(int j=1;j<=p;j++){ int l=0,r=0; for(int i=1;i<=m;i++){ while(l<r&&slope(q[l],q[l+1])<=a[i]) l++; f[i]=g[q[l]]+a[i]*(i-q[l])-(s[i]-s[q[l]]); while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } memcpy(g,f,sizeof(f)); } printf("%lld ",f[m]); return 0; }
9. 「SDOI 2012」任务安排
Problem:(n) 个任务,标号为 (1) 到 (n),第 (i) 个任务单独完成所需的时间是 (t_i)。要求将 (n) 个任务分为若干批,每批包含相邻的若干任务。在每批任务开始前,机器需要启动时间 (s),完成这批任务所需的时间是各个任务需要时间的总和。
同一批任务将在同一时刻完成。每个任务的费用是它的完成时刻乘以一个费用系数 (c_i)。
求最小总费用。(1leq nleq 3 imes 10^5,1leq sleq 2^8,|t_i|leq 2^8,0leq c_ileq 2^8)。
Solution:令 (f_{i,j}) 表示前 (i) 个任务被分为 (j) 批的最小费用。记 (T_i) 表示 (t_i) 的前缀和,(C_i) 表示 (c_i) 的前缀和。
意思就是,第 (j) 批任务(包含 ((k,i]) 的任务)的完成时间为 (T_i+s imes j),这批任务的费用和为 ((T_i+s imes j) imes sum_{p=k+1}^i c_p)。
注意到转移已经是 (mathcal O(1)) 的了,考虑优化 DP 的状态。
发现 (j) 的作用仅是为了计算 (j) 批任务的启动时间和 (s imes j)。将当前这批任务(前 (i) 个任务分完了)分出后,会增加 (s) 等待的启动时间,则后续费用和会增加 (sum_{p=i+1}^n c_p imes s)。考虑提前加进去,从而优化掉状态的第二维。这就是“费用提前计算”的思想。
当 (k<j<i) 时,若 (j) 比 (k) 更优,则有:
直接单调队列维护下凸壳:
int l=0,r=0; for(int i=1;i<=n;i++){ while(l<r&&slope(q[l],q[l+1])<=t[i]) l++; f[i]=f[q[l]]+t[i]*(c[i]-c[q[l]])+s*(c[n]-c[q[l]]); while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; }
然而这样是错的。注意到 (t_i) 可能为负,会导致 (t_i) 的前缀和 (T_i) 不一定单调,这影响了最优决策点的选择,无法使用单调队列取队首 选择最优决策点。
因此不能弹出队首,而是维护整个凸壳,每次查询最优决策点时在凸壳上二分,找到第一个使得左侧斜率小于 (T_i),右侧斜率不小于 (T_i) 的位置即为最优决策点。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=3e5+5; int n,s,x,y,t[N],c[N],f[N],q[N]; int get(int x){ return f[x]-s*c[x]; } double slope(int i,int j){ if(c[i]==c[j]) return 1e18; return 1.0*(get(i)-get(j))/(c[i]-c[j]); } int find(int l,int r,int v){ int ans=r; while(l<=r){ int mid=(l+r)/2; if(slope(q[mid],q[mid+1])>=v) ans=mid,r=mid-1; else l=mid+1; } return q[ans]; } signed main(){ scanf("%lld%lld",&n,&s); for(int i=1;i<=n;i++){ scanf("%lld%lld",&x,&y); t[i]=t[i-1]+x,c[i]=c[i-1]+y; } int l=0,r=0; for(int i=1;i<=n;i++){ int pos=find(l,r,t[i]); f[i]=f[pos]+t[i]*(c[i]-c[pos])+s*(c[n]-c[pos]); while(l<r&&slope(q[r-1],q[r])>=slope(q[r],i)) r--; q[++r]=i; } printf("%lld ",f[n]); return 0; }