斜率DP十连测
最近学校里很多题目都没时间做,顺便来写一下博客
斜率DP十连:
A[征途]
注意到,原题的式子,等价于后面部分是常数
那么我们就可以写出dp方程
对每个i单独做斜率dp,那么式子改写为
斜率为,维护下凸包即可
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,m,q[3010],T; LL f[2][3010],s[3010];
inline LL y(int j){
return f[T^1][j]+s[j]*s[j];
}
inline db slp(int j,int k){
return (y(j)-y(k))/(db)(2.*(s[j]-s[k]));
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i) scanf("%lld",s+i),s[i]+=s[i-1];
for(int i=1;i<=m;++i){
int l=0,r=0; T=i&1;
for(int j=1;j<=n;++j){
while(l<r && slp(q[l],q[l+1])<s[j]) ++l; int k=q[l];
f[T][j]=y(k)+s[j]*s[j]-2*s[j]*s[k];
while(l<r && slp(q[r-1],q[r])>slp(q[r],j)) --r;
q[++r]=j;
}
}
printf("%lld\n",m*f[m&1][n]-s[n]*s[n]);
}
B[货币兑换]
是一个神仙题也
首先注意到一个贪心策略,每天要么全部买入,全部卖出或者什么也不做
好的,注意到这一点之后就可以考虑怎么dp了,设表示到了第i天最多有多少钱
表示到了第i天最多有多少a股,表示同时最多有多少b股
我们有三种转移
1.今天什么也不做
2.今天卖掉从某一天买来的股票
3.买入股票
重点关注第二条式子,我们将其改写:
这里b是常数可以不管,只需要维护上凸包,让后在凸包上二分斜率即可
但是发现不是单调的,我们需要选择以下一项:
1.splay维护凸包
2.cdq分治
前者明显比后者直观易懂,所以我们用splay维护凸包,过程和用队列差不多,只是如果一个点被加入到了凸包内部需要直接删除,而加入的有用的点,直接找它左边和右边第一个满足上凸的点,并将中间的都删掉即可
#include<math.h>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 100010
#define db double
#define eps 1e-9
#define inf 1e9
#define son(x) (s[f[x]][1]==x)
using namespace std;
int n,s[N][2],v[N],f[N],rt;
db l[N],r[N],a[N],b[N],X[N],Y[N],g[N],rat[N];
inline db slp(int j,int k){
return fabs(X[j]-X[k])<eps?-inf:(Y[j]-Y[k])/(double)(X[j]-X[k]);
}
inline void rot(int x){
int y=f[x],d=son(x),g=f[y];
s[y][d]=s[x][!d]; f[s[y][d]]=y;
if(g) s[g][son(y)]=x; f[x]=g;
s[x][!d]=y; f[y]=x;
}
inline void splay(int x,int r=0){
for(int p;(p=f[x])!=r;rot(x))
if(f[p]!=r && son(x)==son(p)) rot(p);
if(!r) rt=x;
}
inline void insert(int k){
if(!rt){ rt=k; return; }
for(int x=rt;;){
int d=X[x]<X[k];
if(!s[x][d]){ s[x][d]=k; f[k]=x; splay(k); return; }
else x=s[x][d];
}
}
inline int gPre(int k){
int r=0;
for(int x=s[k][0];x;){
if(slp(x,k)<=l[x]+eps) r=x,x=s[x][1];
else x=s[x][0];
}
return r;
}
inline int gSuc(int k){
int l=0;
for(int x=s[k][1];x;){
if(slp(x,k)+eps>=r[x]) l=x,x=s[x][0];
else x=s[x][1];
}
return l;
}
inline void ps(int k){
int p;
if(s[k][0]){
p=gPre(k);
splay(p,k);
f[s[p][1]]=0;
s[p][1]=0;
l[k]=r[p]=slp(k,p);
} else l[k]=inf;
if(s[k][1]){
p=gSuc(k);
splay(p,k);
f[s[p][0]]=0;
s[p][0]=0;
r[k]=l[p]=slp(k,p);
} else r[k]=-inf;
}
inline void maintain(int k){
splay(k); ps(k); int p;
if(l[k]<=r[k]+eps){
if(!s[k][0] || !s[k][1]){
rt=s[k][0]+s[k][1];
f[rt]=0; ps(rt);
} else {
p=gSuc(k);
splay(p,k);
rt=s[k][0];
f[rt]=0;
s[rt][1]=p;
f[p]=rt;
ps(p); ps(rt);
}
}
}
inline void out(int x){
if(!x) return;
out(s[x][0]);
printf("%d ",x);
out(s[x][1]);
}
inline int find(db k){
if(!rt) return 0;
int x=rt;
for(;;){
if(r[x]>k+eps) x=s[x][1]; else
if(l[x]+eps<k) x=s[x][0];
else return x;
}
}
int main(){
scanf("%d%lf",&n,g);
for(int i=1;i<=n;++i) scanf("%lf%lf%lf",a+i,b+i,rat+i);
for(int i=1;i<=n;++i){
int j=find(-a[i]/b[i]);
g[i]=max(g[i-1],a[i]*X[j]+b[i]*Y[j]);
// printf("%d\n",j);
Y[i]=g[i]/(a[i]*rat[i]+b[i]);
X[i]=Y[i]*rat[i]; insert(i); maintain(i);// out(rt); puts("");
}
for(int i=n;i<=n;++i) printf("%.3lf\n",g[i]);
}
C[柠檬]
不得不说这题如果想到了关键一步就基本秒掉了,然而我并没有想到
首先考虑一下怎么转移式子
设表示前i个贝壳能转化为多少柠檬,枚举j,发现各种无法转移
因为我们不可能去枚举那怎么办呢
注意到决策最优性,如果对于某一段,我们选取大小为的贝壳去做转化,那么这一段的开头和结尾必然也是否则我们将其分成独立的一段,一定更优
这下马上可以转移了,写成方程就是
这里贝壳i的大小,表示的是i是第几个大小为的贝壳
那么对于每个v[i]我们维护一个单独的上凸壳,每次询问在上面二分即可,这里用vector节省空间
#include<vector>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
#define db double
using namespace std;
vector<int> q[10010];
int n,c[10010],v[100010]; LL f[100010],s[100010];
inline LL y(int j){
return f[j-1]+v[j]*s[j]*s[j]-2*s[j]*v[j];
}
inline db slp(int j,int k){
return (y(j)-y(k))/(2.*v[j]*(s[j]-s[k]));
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",v+i);
s[i]=s[c[v[i]]]+1; c[v[i]]=i;
}
// for(int i=1;i<=n;++i){
// for(int j=1;j<=i;++j) if(v[i]==v[j])
// g[i]=max(g[i],g[j-1]+v[i]*(s[i]-s[j]+1)*(s[i]-s[j]+1));
// }
// for(int i=1;i<=n;++i) if(s[i]==1) q[v[i]].push_back(i);
for(int i=1,j,k;i<=n;++i){
k=v[i];
if(q[k].size()<=1) q[k].push_back(i); else{
for(int j=q[k].size()-1;j;--j)
if(slp(q[k][j],q[k][j-1])<slp(q[k][j],i)) q[k].pop_back(); else break;
q[k].push_back(i);
}
if(q[k].size()==1) j=q[k][0];
else {
int l=0,r=q[k].size()-1;
for(int m;l<r;){
m=l+r>>1;
if(slp(q[k][m],q[k][m+1])>=s[i]) l=m+1;
else r=m;
}
j=q[k][l];
}
f[i]=f[j-1]+v[i]*(s[i]-s[j]+1)*(s[i]-s[j]+1);
}
printf("%lld\n",f[n]);
}
D[玩具装箱]
题目难度从此开始下降
对于题目里面已经给出的式子,我们直接把它翻译成代码即可
这里写一下转化式子的步骤,设k<j<i
后面题目不再重复上述过程
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define N 50010
#define LL long long
using namespace std;
int n,L;
LL s[N],g[N],f[N],q[N];
inline db y(int j){
return f[j]+g[j]*g[j]+2*g[j]*L;
}
inline db slp(int j,int k){ return (y(j)-y(k))/(2.*(g[j]-g[k])); }
int main(){
scanf("%d%d",&n,&L); ++L;
for(int i=1;i<=n;++i){
scanf("%lld",s+i);
s[i]+=s[i-1]; g[i]=s[i]+i;
}
int l=0,r=0; f[0]=0;
for(int i=1,j;i<=n;++i){
while(l<r && slp(q[l],q[l+1])<=g[i]) ++l; j=q[l];
f[i]=f[j]+(g[i]-g[j]-L)*(g[i]-g[j]-L);
while(l<r && slp(q[r-1],q[r])>slp(q[r],i)) --r; q[++r]=i;
}
printf("%lld\n",f[n]);
}
E[仓库建设]
经典题目之一
这个题目我们考虑倒着做,因为直接计算距离不是非常方便
先计算出只建立一个仓库的代价之和S
设表示最后一个仓库放在i的最大节约代价
那么就可以得到一条非常简洁的转移式子
这里是第i个工厂到第n个工厂的距离,是1~i个工厂的产品数量之和
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define N 1000010
#define LL long long
using namespace std;
int n,r[N],p[N],c[N],q[N];
LL s[N],f[N],S=0,g[N];
inline db slp(int j,int k){
return (f[j]-f[k])/(db)(r[k]-r[j]);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d%d%d",r+i,p+i,c+i),s[i]=s[i-1]+p[i];
for(int i=1;i<n;++i) S+=(r[n]-r[i])*(LL)p[i]; S+=c[n];
f[n]=0; int h=1,t=0; q[++t]=n;
for(int i=n-1;i;--i){
while(h<t && slp(q[h],q[h+1])>=s[i]) ++h;
f[i]=f[q[h]]+(r[q[h]]-r[i])*s[i]-c[i];
while(h<t && slp(q[t],q[t-1])<slp(q[t],i)) --t;
q[++t]=i; *f=max(*f,f[i]);
}
printf("%lld\n",S-*f);
}
F[Cats Transport]
终于来了几道CF的题
和上面一道题一样,我们依然先计算出只用一个feeder时的等待时间之和
让后再计算能优化多少,设表示用了i个feeder,最后一个在时刻j出发的答案
那么就得到了和上题几乎一模一样的式子
注意用滚动数组就可以了,基本没有什么变化
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 100010
#define db double
#define LL long long
using namespace std;
int n,m,k,q[N]; LL f[2][N],s[N],S,r[N],p[N],d[N],g[10][1000];;
inline db slp(int j,int k){ return (f[0][j]-f[0][k])/(r[k]-r[j]); }
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=2;i<=n;++i) scanf("%lld",d+i),d[i]+=d[i-1];
for(int x,i=1;i<=m;++i){
scanf("%d%lld",&x,r+i); r[i]-=d[x];
}
sort(r+1,r+1+m); n=0; r[0]=-1000;
for(int i=1;i<=m;++i){
if(r[i]!=r[i-1]){ r[++n]=r[i]; p[n]=1; }
else ++p[n];
}
for(int i=1;i<n;++i){
S+=(r[n]-r[i])*(LL)p[i];
s[i]=s[i-1]+p[i];
}
for(int T=1;T<k;++T){
f[0][n]=0; int h=1,t=0; q[++t]=n;
for(int i=n-1;i;--i){
while(h<t && slp(q[h],q[h+1])>=s[i]) ++h;
f[1][i]=f[0][q[h]]+(r[q[h]]-r[i])*s[i];
while(h<t && slp(q[t],q[t-1])<slp(q[t],i)) --t;
q[++t]=i;
}
swap(f[0],f[1]);
}
LL A=0;
for(int i=1;i<=n;++i) A=max(A,f[0][i]);
printf("%lld\n",S-A);
}
G[Product Sum]
也是一个比较简单的题
我们设表示移动i能获得最大的贡献
那么考虑把这个i移动到j这个位置,令就得到转移式子
注意到这里并没有i<j的限制,所以我们要选择一项:
1.做两次
2.先求凸包再二分
肯定是第二项比较方便辣
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,m,q[200010];
LL f[200010],v[200010],s[200010],S;
inline db slp(int j,int k){
return (s[j]-s[k])/(j-k);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%lld",v+i),s[i]=s[i-1]+v[i],S+=v[i]*i;;
int t=0; q[++t]=0;
for(int i=1;i<=n;++i){
while(1<t && slp(q[t-1],q[t])>slp(q[t],i)) --t;
q[++t]=i;
}
for(int i=1;i<=n;++i){
int l=1,r=t;
for(int m;l<r;){
m=l+r>>1;
if(slp(q[m],q[m+1])<v[i]) l=m+1;
else r=m;
}
f[i]=s[i]-s[q[l]]+(q[l]-i)*v[i];
*f=max(*f,f[i]);
}
printf("%lld\n",*f+S);
}
H[Levels and Regions]
这个题需要推一推式子,然而我期望这方面技巧极差
首先推一推打通第i关所需要的时间Ti,设i所在的Region为[l,r]
那么
设那么就得到就是
那么整个Region的期望时间E(l,r)就是
为了方便dp,我们将这个式子转化一下,首先设
这里预处理一下就可以了
最后的dp式子是这样的:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define N 200010
#define db double
using namespace std;
int n,k,q[N];
db DP[2][N],AA[N],BB[N],s[N],v[N];
inline db y(int j){
return DP[0][j]-AA[j]+s[j]*BB[j];
}
inline db slp(int j,int k){
return (y(j)-y(k))/(s[j]-s[k]);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i){
scanf("%lf",v+i);
s[i]=s[i-1]+v[i];
AA[i]=AA[i-1]+s[i]/v[i];
BB[i]=BB[i-1]+1./v[i];
}
for(int i=1;i<=n;++i) DP[0][i]=AA[i];
for(int T=2;T<=k;++T){
int l=1,r=0; q[++r]=0;
for(int i=1,j;i<=n;++i){
while(l<r && slp(q[l],q[l+1])<BB[i]) ++l;
j=q[l];
DP[1][i]=DP[0][j]+AA[i]-AA[j]-s[j]*(BB[i]-BB[j]);
while(l<r && slp(q[r-1],q[r])>=slp(q[r],i)) --r;
q[++r]=i;
}
swap(DP[0],DP[1]);
}
printf("%.7lf\n",DP[0][n]);
}
I[序列分割]
回到Bzoj了
发现这个题,无论什么顺序分割序列,最终答案只和分割位置有关
那么计算一下发现就是每个块两两相乘,得到式子
设表示做到j,用了i块的最大结果
滚动数组就没了
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,k,q[100010]; LL f[2][100010],s[100010];
inline LL y(int j){ return f[0][j]-s[j]*s[j]; }
inline db slp(int j,int k){
if(s[k]==s[j]) return 1e100;
return (y(j)-y(k))/(s[k]-s[j]);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i) scanf("%lld",s+i),s[i]+=s[i-1];
for(int i=1;i<=k;++i){
int l=1,r=0; q[++r]=0;
for(int j=1;j<=n;++j){
while(l<r && slp(q[l],q[l+1])<s[j]) ++l;
f[1][j]=f[0][q[l]]+(s[j]-s[q[l]])*s[q[l]];
while(l<r && slp(q[r],q[r-1])>slp(q[r],j)) --r;
q[++r]=j;
}
swap(f[0],f[1]);
}
printf("%lld\n",f[0][n]);
}
J[特别行动队]
前面那么多例题,这个题就不说了吧,注意a是负的就行了
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
#define LL long long
using namespace std;
int n,m,q[1000010],a,b,c;
LL f[1000010],v[1200010],s[1200010],S;
inline LL y(int j){ return f[j]+a*s[j]*s[j]-b*s[j]; }
inline db slp(int j,int k){
return (y(j)-y(k))/(s[j]-s[k]);
}
int main(){
scanf("%d%d%d%d",&n,&a,&b,&c);
for(int i=1;i<=n;++i) scanf("%lld",v+i),s[i]=s[i-1]+v[i];
int l=1,r=0; q[++r]=0;
for(int i=1;i<=n;++i){
while(l<r && slp(q[l],q[l+1])>2.*a*s[i]) ++l;
int j=q[l];
f[i]=f[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c;
while(l<r && slp(q[r-1],q[r])<slp(q[r],i)) --r; q[++r]=i;
}
printf("%lld\n",f[n]);
}