斜率优化dp
1) 转移方程类似 \(f[i]=a[i]+b[j],j<=i\) 的可以用单调队列维护\(i\)前面的一段合法区间的最值,从而将\(O(n^2)\)的转移优化到\(O(n)\)
2) 转移方程类似\(f[i]=a[i]* b[j]+c[i]+d[j]\)就不能只是单调队列优化了,我们需要单调队列+斜率优化
先来一道例题会比较好理解
P3195
经典斜率优化。 $$
转移方程为f[i]=f[j]+(i-j-1+s[i]-s[j]-L)\s[i]表示前缀和。\
设a(i)=s[i]+i,b(i)=a(i)+L+1\那么原式变为f[i]=f[j]+(a(i)-b(j))^2\ 移项得\
2a(i)* b(j) +f[i]-a(i)^2=f[j]+b(j)^2\
那么可以看成k=2a(i),x=b(j),y=(f[j]+b(j)^2),b=f[i]-a(i)^2的直线y=kx+b $$
对于每个直线(1~n),都有斜率\(2a(i)\)单调递增,而我们要求截距\(f[i]-a(i)^2\)的最小值,也就是当我们维护一个下凸包时,直线的斜率刚好满足\(slope(P_{j},P_{j+1})>k\)时的点\(j\)即为我们要的\(f[j]\)(可以理解为相切)
因此用单调队列维护下凸包上的点。
1) \(slope(P_{head},P_{head+1}\leq k):++head\),弹出前端的点,因为不"相切"
2) \(slope(P_{tail-1},P_i)<slope(P_{tail-1},P_{tail}):--tail\),说明\(P_{tail}\)不再是凸包上的点,不需要就弹出
#include<iostream>
#include<cstdio>
#include<cstring>
#define int long long
using namespace std;
typedef double db;
const int N=5e4+10;
int C[N],s[N],q[N];
int n,L,head,tail;
db f[N];
db a(int i){
return (db)(s[i]+i);
}
db b(int i){
return (db)(s[i]+i+L+1);
}
db slope(int u,int v){
db yu=f[u]+b(u)*b(u),yv=f[v]+b(v)*b(v),xu=b(u),xv=b(v);
return (yu-yv)/(xu-xv);
}
void dp(){
head=tail=1;
q[head]=0;
for(int i=1;i<=n;++i){
while(head<tail && slope(q[head],q[head+1])< 2*a(i)) ++head;
f[i]=f[q[head]]+(a(i)-b(q[head]))*(a(i)-b(q[head]));
while(head<tail && slope(q[tail-1],q[tail])>slope(q[tail-1],i)) --tail;
q[++tail]=i;
}
printf("%lld",(long long)(f[n]));
}
signed main(){
scanf("%lld%lld",&n,&L);
for(int i=1;i<=n;++i) scanf("%lld",&C[i]),s[i]=s[i-1]+C[i];
dp();
return 0;
}
线段树优化dp
P1295 & P1848
相同类型的题,就只讲P1295吧,还简单一点。
暴力\(O(n^2)\)转移:\(f[i]=f[j]+(\max_{k=j+1}^ih_k)(\sum_{k=j+1}^ih_k\leq m)\)
对于每个点,可以首先\(O(n\log n)\)二分求出它最靠左且满足长度\(\leq m\)的端点位置\(prelen[i]\),还有\(O(n)\)单调栈求出前一个比他大的数的位置 \(premax[i]\).
然后,对于当前点\(i\),我们需要将前\(i-1\)个点的\(f[i]\)加上\(i\)产生的"贡献"后取最小,求出\(f[i]\).
所以,我们需要在求\(f[k]\)之前在\(k\)的位置单点修改\(f[k-1]\),表示如果取\([k,i]\)这段区间的话要加上\(f[k-1]\).
然后就是常见的操作,对于所有大于\(h[i]\)的点是没有影响的,所以将\([premax[i],i]\)修改为\(h[i]\).
最后,查询\([prelen[i],i]\)这段区间的最小值即可。
显然这个过程可以用线段树维护。
注意\(f\)是单点修改,不需要\(tag\),而\(ans\)需要用\(tag+f\)得到。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<stack>
#define int long long
#define ls i<<1
#define rs i<<1|1
#define mid (l+r>>1)
using namespace std;
typedef pair<int,int> PI;
const int N=1e5+10,INF=1e18;
int n,m;
int a[N],premax[N],prelen[N],s[N],f[N];
struct tree{
int tag,l,r,f,ans;
}tre[N<<2];
void init(){
stack<PI> q;
for(int i=1;i<=n;++i){
int pos=lower_bound(s,s+n+1,s[i]-m)-s+1;prelen[i]=pos;
}
for(int i=1;i<=n;++i){
while(!q.empty() && q.top().first<a[i])q.pop();
premax[i]=q.empty()?1:q.top().second+1;q.push(make_pair(a[i],i));
}
}
void pushup(int i){
tre[i].f=min(tre[ls].f,tre[rs].f);
tre[i].ans=min(tre[ls].ans,tre[rs].ans);
}
void build(int i,int l,int r){
tre[i].l=l,tre[i].r=r,tre[i].tag=0,tre[i].f=INF,tre[i].ans=INF;
if(l==r)return;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(i);
}
void pushdown(int i){
int k=tre[i].tag;
if(!k)return;
tre[ls].tag=k,tre[ls].ans=tre[ls].f+k;
tre[rs].tag=k,tre[rs].ans=tre[rs].f+k;
tre[i].tag=0;
}
void add(int i,int pos,int k){
int l=tre[i].l,r=tre[i].r;
if(l==r){
tre[i].f=k;return;
}
pushdown(i);
if(pos<=mid)add(ls,pos,k);
else add(rs,pos,k);
pushup(i);
}
void change(int i,int el,int er,int k){
int l=tre[i].l,r=tre[i].r;
if(el<=l && r<=er){
tre[i].ans=tre[i].f+k,tre[i].tag=k;
return;
}
pushdown(i);
if(el<=mid)change(ls,el,er,k);
if(er>mid)change(rs,el,er,k);
pushup(i);
}
int query(int i,int el,int er){
int l=tre[i].l,r=tre[i].r;
if(el<=l && r<=er) return tre[i].ans;
pushdown(i);
int ans=INF;
if(el<=mid)ans=min(ans,query(ls,el,er));
if(er>mid)ans=min(ans,query(rs,el,er));
return ans;
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i)scanf("%lld",&a[i]),s[i]=s[i-1]+a[i];
build(1,1,n);
init();
for(int i=1;i<=n;++i){
add(1,i,f[i-1]);
if(premax[i]<=i)change(1,premax[i],i,a[i]);
if(prelen[i]<=i)f[i]=query(1,prelen[i],i);
}
printf("%lld",f[n]);
return 0;
}
/*
4 9
1 6 3 5 1
*/
CF833B
常见离线操作,对于\(a[i]\)只对它前一次出现的位置之后有影响。
而对于那个\(k\)段,可以每次建一次树,树中初始为\(f[l-1][k-1]\)。
然后就是标准线段树优化。
#include<iostream>
#include<cstdio>
#include<cstring>
#define ls i<<1
#define rs i<<1|1
#define int long long
using namespace std;
const int N=4e4+10,K=55;
int f[K][N],pos[N],pre[N],tre[N<<2],tag[N<<2];
int n,k,t;
void pushup(int i){
tre[i]=max(tre[ls],tre[rs]);
}
void pushdown(int i,int l,int r){
int k=tag[i];
if(!k)return;
tag[ls]+=k;tag[rs]+=k;tre[ls]+=k;tre[rs]+=k;
tag[i]=0;
}
void build(int i,int l,int r,int now){
tag[i]=0;
if(l==r){
tre[i]=f[now][l-1];
return;
}
int mid=(l+r)>>1;
build(ls,l,mid,now);
build(rs,mid+1,r,now);
pushup(i);
}
void change(int i,int l,int r,int el,int er,int k){
if(el<=l && r<=er){
tag[i]+=k;tre[i]+=k;
return;
}
pushdown(i,l,r);
int mid=(l+r)>>1;
if(el<=mid) change(ls,l,mid,el,er,k);
if(er>mid) change(rs,mid+1,r,el,er,k);
pushup(i);
}
int query(int i,int l,int r,int el,int er){
if(el<=l && r<=er) return tre[i];
pushdown(i,l,r);
int mid=(l+r)>>1,ans=0;
if(el<=mid)ans=max(ans,query(ls,l,mid,el,er));
if(er>mid)ans=max(ans,query(rs,mid+1,r,el,er));
return ans;
}
signed main(){
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;++i)scanf("%lld",&t),pre[i]=pos[t]+1,pos[t]=i;
for(int i=1;i<=k;++i){
build(1,1,n,i-1);
for(int j=1;j<=n;++j){
change(1,1,n,pre[j],j,1);
f[i][j]=query(1,1,n,1,j);
}
}
printf("%lld",f[k][n]);
}