DP的优化
一、数据结构优化DP
P3287 [SCOI2014] 方伯伯的玉米田
首先容易分析出一个性质:拔高玉米时,拔高
设
状态转移方程:
然后你就可以拿到10分的好成绩
考虑优化。我们发现,状态转移需要求二维前缀的最大值,所以考虑二维树状数组优化。然而,
设
状态转移方程:
然后就可以愉快地使用二维树状数组了。
int ask(int x,int y){
int re=0;
for(int i=x;i;i-=i&(-i))
for(int j=y;j;j-=j&(-j))
re=max(re,dp[i][j]);
return re;
}
void add(int x,int y,int z){
for(int i=x;i<=k+1;i+=i&(-i))
for(int j=y;j<=k+maxn;j+=j&(-j))
dp[i][j]=max(dp[i][j],z);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]),maxn=max(maxn,a[i]);
for(int i=1;i<=n;++i){
for(int j=0;j<=k;++j){
int t=ask(j+1,a[i]+j)+1;//这里写j+1是避免j==0时树状数组死循环
now[j]=t;
ans=max(ans,t);
}
for(int j=0;j<=k;++j)
add(j+1,a[i]+j,now[j]);
}
/*for(int i=1;i<=n;++i)//注释掉的部分是暴力DP
for(int j=0;j<=k;++j){
for(int l=0;l<i;++l){
if(a[l]>a[i]&&a[l]-a[i]<=j)
dp[i][j]=max(dp[i][j],dp[l][j-(a[l]-a[i])]+1);
else if(a[l]<=a[i])
dp[i][j]=max(dp[i][j],dp[l][j]+1);
}*/
printf("%d\n",ans);
return 0;
}
P6773命运
设
状态转移方程:
第一个和式是
然后可以用前缀和表示一下。设
然后用线段树合并维护它就好了。
void add(int x,int y){
nxt[++tot]=head[x];head[x]=tot;ver[tot]=y;
}
void push_down(int u){//mul是乘法懒标记
p[p[u].l].sum=p[p[u].l].sum*p[u].mul%mod;
p[p[u].l].mul=p[p[u].l].mul*p[u].mul%mod;
p[p[u].r].sum=p[p[u].r].sum*p[u].mul%mod;
p[p[u].r].mul=p[p[u].r].mul*p[u].mul%mod;
p[u].mul=1;
}
long long ask(int u,int ul,int ur,int pos){
if(!u)
return 0;
if(ur<=pos)
return p[u].sum;
int mid=(ul+ur)>>1;long long re=0;
push_down(u);
if(mid>=pos)
re=(re+ask(p[u].l,ul,mid,pos))%mod;
if(mid<pos){
re=(re+ask(p[u].l,ul,mid,pos))%mod;
re=(re+ask(p[u].r,mid+1,ur,pos))%mod;
}
return re;
}
int merge(int u,int v,int ul,int ur,long long &s1,long long &s2){//s1:f[v][1...d[u]]+f[v][1...i]; s2:f[u][1...i-1]
if(!u&&!v)
return 0;
if(!u||!v){
if(!u){//f[u][i]<-s2*f[v][i]
s1=(s1+p[v].sum)%mod;
p[v].mul=p[v].mul*s2%mod;
p[v].sum=p[v].sum*s2%mod;
return v;
}
else{//f[u][i]<-f[u][i]*s1
s2=(s2+p[u].sum)%mod;
p[u].sum=p[u].sum*s1%mod;
p[u].mul=p[u].mul*s1%mod;
return u;
}
}
if(ul==ur){//f[u][i]<-s1*f[u][i]+s2*f[v][i]
long long tmp=p[u].sum,tmp2=p[v].sum;
s1=(s1+tmp2)%mod;
p[u].sum=((p[u].sum*s1%mod)+(p[v].sum*s2%mod))%mod;
s2=(s2+tmp)%mod;
return u;
}
push_down(u);push_down(v);
int mid=(ul+ur)>>1;
p[u].l=merge(p[u].l,p[v].l,ul,mid,s1,s2);
p[u].r=merge(p[u].r,p[v].r,mid+1,ur,s1,s2);
p[u].sum=(p[p[u].l].sum+p[p[u].r].sum)%mod;
return u;
}
void add(int &u,int ul,int ur,int pos){//新开一个线段树
if(!u) u=++cnt;
p[u].sum=p[u].mul=1;
if(ul==ur)
return ;
int mid=(ul+ur)>>1;
if(mid>=pos)
add(p[u].l,ul,mid,pos);
if(mid<pos)
add(p[u].r,mid+1,ur,pos);
}
void dfs(int x,int fa){
d[x]=d[fa]+1;
int maxx=0;
for(int i=0;i<v[x].size();++i)
maxx=max(maxx,d[v[x][i]]);//找到最深的未被满足的上端点的深度
add(rt[x],0,n,maxx);
for(int i=head[x];i;i=nxt[i]){
if(ver[i]!=fa){
dfs(ver[i],x);
long long s=ask(rt[ver[i]],0,n,d[x]),ss=0;//s求出f[ver[i]][1...d[x]]
rt[x]=merge(rt[x],rt[ver[i]],0,n,s,ss);
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;++i){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
scanf("%d",&m);
for(int i=1;i<=m;++i){
scanf("%d%d",&x,&y);//anc[y]=x
v[y].push_back(x);
}
dfs(1,0);
printf("%lld\n",ask(rt[1],0,n,0));
return 0;
}
二、单调队列优化DP
对于形如
①:
②:
则可以使用单调队列优化。
P2300 合并神犇
一个不太常规的单调队列优化DP。
设
状态转移方程:
接下来考虑优化。
引理:
证明:假设原序列为
观察状态转移方程,再结合引理,可以发现当
对
因此我们可以维护一个
l=1;r=0;
for(int i=1;i<=n;++i){
while(l<=r&&pre[q[l]]+sum[q[l]]<=sum[i])
++l;
f[i]=f[q[l-1]]+i-q[l-1]-1;
pre[i]=sum[i]-sum[q[l-1]];
while(l<=r&&sum[q[r]]+pre[q[r]]>=sum[i]+pre[i])
--r;
q[++r]=i;
}
printf("%lld\n",f[n]);
三、斜率优化DP
在上文中,如果把条件“每一项都只和
具体地,将
令
接下来我们以
对于任意三个决策点
①:当
②:当
③:如果二者都不单调递增,则需要用平衡树维护凸包(动态凸包),或者李超树。
三、四边形不等式优化DP
四边形不等式:
对于函数
形象点说,就是“包含大于等于交叉”。
一维DP
前置知识(决策单调性的定义):对于形如
定理一:在上述状态转移方程中,如果
技巧:二分队列
首先,我们用三元组将决策点数组进行替换。具体而言,就是说如果
对于每个
①:更新
②:考虑
(1):如果对于
(2):如果对于
(3):将
P1912 [NOI2009] 诗人小G
设
因为代价函数存在高次乘积,所以不宜使用单调队列或者斜率优化。
打表可知,代价函数满足四边形不等式,因此
long long _get(long long i,long long l,long long r){
long long ans;
while(l<=r){
int mid=(l+r)>>1;
if(q[mid].l<=i&&q[mid].r>=i){
ans=mid;
break;
}
if(i>=q[mid].l)
l=mid+1;
else
r=mid-1;
}
return q[ans].x;
}
long double val(long long i,long long j){
long double ans=1,w=abs((long double)(sum[i]-sum[j]+i-j-1-len));
for(int i=1;i<=p;++i)
ans*=w;
return ans+f[j];
}
void insert(long long i,long long &l,long long &r){
int pos=-1;
while(l<=r){//从队尾开始往前查找,决策点单调不增
if(val(q[r].l,i)<=val(q[r].l,q[r].x))
pos=q[r].l,--r;//该元素代表的区间的全部决策点都为i
else{
if(val(q[r].r,q[r].x)>val(q[r].r,i)){//该元素代表的区间存在决策点i
int l2=q[r].l,r2=q[r].r;
while(l2<r2){//二分查找,[q[r].l,l2-1]的决策点不变,[l2,n]的决策点为i
int mid=(l2+r2)>>1;
if(val(mid,i)>val(mid,q[r].x))
l2=mid+1;
else
r2=mid;
}
q[r].r=l2-1;
pos=l2;
}
break;//该元素代表的区间的决策点都不是i,直接停止循环
}
}
if(pos!=-1){
q[++r].l=pos;
q[r].r=n;
q[r].x=i;
}
}
void print(int now){
if(!now)
return ;
print(pre[now]);
for(int i=pre[now]+1;i<=now;++i){
cout<<s[i];
if(i!=now) printf(" ");
}
cout<<endl;
}
void work(){
scanf("%lld%lld%lld",&n,&len,&p);
for(int i=1;i<=n;++i){
cin>>s[i];
sum[i]=sum[i-1]+s[i].size();
}
q[l=r=1].l=1;q[1].r=n;q[1].x=0;
for(int i=1;i<=n;++i){
long long j=_get(i,l,r);
f[i]=val(i,j);
pre[i]=j;
while(l<=r&&q[l].r<=i)
++l;
q[l].l=i+1;
insert(i,l,r);
}
if(f[n]>1e18)
puts("Too hard to arrange");
else{
cout<<(long long)f[n]<<endl;
print(n);
}
for(int i=1;i<=20;++i)
printf("-");
printf("\n");
}
int main(){
scanf("%lld",&t);
while(t--)
work();
return 0;
}
二维DP
定理二:在形如
①:四边形不等式;
②:区间包含单调性,即对于任意的
那么:
①:
②:对于
因此对于
P4767 邮局
模版题。首先将村庄按照坐标从小到大排序。设 打表可知,代价函数满足四边形不等式和区间包含点调性。
如何计算
int w(int l,int r){
int mid=(l+r+1)>>1;
return a[mid]*(mid-l+1)-(sum[mid]-sum[l-1])+(sum[r]-sum[mid])-a[mid]*(r-mid);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
sort(a+1,a+n+1);
for(int i=1;i<=n;++i)
sum[i]=sum[i-1]+a[i];
memset(f,0x3f,sizeof(f));
f[0][0]=0;
for(int j=1;j<=m;++j){
opt[n+1][j]=n;
for(int i=n;i>=1;--i){
int pos=0;
for(int k=opt[i][j-1];k<=opt[i+1][j];++k){
if(f[k][j-1]+w(k+1,i)<f[i][j]){
f[i][j]=f[k][j-1]+w(k+1,i);
pos=k;
}
}
opt[i][j]=pos;
}
}
printf("%d\n",f[n][m]);
return 0;
}
技巧:分治法处理 函数难以计算的情况
如果
设
每次更新时,运用双指针思想暴力移动
找到
P5574 [CmdOI2019] 任务分配问题
设 打表可知,代价函数满足四边形不等式和区间包含点调性。
然后就可以直接运用上述技巧了。
void add(int x,int w){
while(x<=n)
c[x]+=w,x+=x&(-x);
}
int ask(int x){
int re=0;
while(x)
re+=c[x],x-=x&(-x);
return re;
}
void update(int l,int r){
while(tr<r)
++tr,sum+=ask(a[tr]-1),add(a[tr],1);
while(tl>l)
--tl,sum+=ask(n)-ask(a[tl]),add(a[tl],1);
while(tr>r)
sum-=ask(a[tr]-1),add(a[tr],-1),--tr;
while(tl<l)
sum-=ask(n)-ask(a[tl]),add(a[tl],-1),++tl;
}
void solve(int l,int r,int l2,int r2,int j){
if(l>r)
return ;
int mid=(l+r)>>1,p=mid;
for(int i=min(mid-1,r2);i>=l2;--i){
update(i+1,mid);
if(f[mid][j]>=f[i][j-1]+sum)
f[mid][j]=f[i][j-1]+sum,p=i;
}
solve(mid+1,r,p,r2,j);solve(l,mid-1,l2,p,j);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
memset(f,0x3f,sizeof(f));
f[0][0]=0;
tl=1;tr=0;
for(int i=1;i<=m;++i)
solve(1,n,0,n-1,i);
printf("%d\n",f[n][m]);
return 0;
}
其他技巧
Array Beauty
2022.7.21拷逝题。
首先将原序列排序,不会对答案造成影响。
设
状态转移方程:
其中,
然而,这样写时间复杂度为
设所有数的值域为
继续优化。如果我们先枚举
void work(int v){
f[0][0]=1;a[0]=-1e9;
for(int i=1;i<=k;++i){
int p=0,sum=0;
for(int j=i;j<=n;++j){
while(p<j&&a[j]-a[p]>=v)
sum=(sum+f[i-1][p])%mod,++p;
f[i][j]=sum;
}
}
for(int i=k;i<=n;++i)
ans[v]=(ans[v]+f[k][i])%mod;
}
signed main(){
scanf("%lld%lld",&n,&k);
for(int i=1;i<=n;++i)
scanf("%lld",&a[i]),maxn=max(maxn,a[i]);
sort(a+1,a+n+1);
for(int v=1;v*(k-1)<=maxn;++v){
work(v);
}
for(int i=1;i*(k-1)<=maxn;++i)
sum=(sum+(ans[i]-ans[i+1]+mod)%mod*i%mod)%mod;
printf("%lld\n",sum);
fclose(stdin);fclose(stdout);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】