区间DP
区间DP的特征一般很明显——数据范围。一般而言,朴素的区间DP数据范围只有1e3~1e4,只有加上一些毒瘤的数据结构的题才会更大
状态设计上,区间DP都比较套路。\(f[l][r]\) 与 \(f[l][r][0/1]\) 都是非常常见的。不过具体问题具体分析,压缩维度等常用思想仍旧重要
转移方程是区间DP的难点,因为状态数多,很多时候需要分类讨论。根据题目要求,注意取max、min、取膜等条件列出方程
P1220 关路灯
原题极小的范围甚至能满足 \(O(n^4)\) 的算法,但是正解复杂度正常是 \(O(n^2)\) 级别的,只能说数据还是太水了,加强一下就可以了。
考虑 \(f_{l,r,0/1}\) 表示区间 \(l,r\) 中的所有灯都关完后站在左端点或右端点所需要的最小电力消耗。
当然这里的 \(l,r\) 指的是灯的编号。考虑一下如何去转移。由于你在关某个灯时,其它所有没关的灯仍然亮着,因此都要考虑周全
设 \(sum_{l}\) 表示前缀功率,\(loc_i\) 表示编号为 \(i\) 的灯的真实位置,有
这道题我DP的写法是记忆化搜索,稍微好理解一些,不需要去管循环的顺序那些,当然常数大一些。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=105;
int n,loc[N],w[N],sum[N],c,f[N][N][2],inf=1e9+7;
int solve(int l,int r,bool x)
{
if(l<1||r>n||l>r) return inf;
if(f[l][r][x]!=inf||l==r) return f[l][r][x];
int res=inf;
if(x==0)
res=min({res,solve(l+1,r,x)+(loc[l+1]-loc[l])*(sum[n]+sum[l]-sum[r]),solve(l+1,r,x^1)+(loc[r]-loc[l])*(sum[n]+sum[l]-sum[r])});
if(x==1)
res=min({res,solve(l,r-1,x)+(loc[r]-loc[r-1])*(sum[n]+sum[l-1]-sum[r-1]),solve(l,r-1,x^1)+(loc[r]-loc[l])*(sum[n]+sum[l-1]-sum[r-1])});
f[l][r][x]=min(res,inf-1);return res;
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>c;
for(int i=1;i<=n;i++) cin>>loc[i]>>w[i],sum[i]=sum[i-1]+w[i];
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) f[i][j][0]=f[i][j][1]=inf;
f[c][c][0]=f[c][c][1]=0;
cout<<min(solve(1,n,0),solve(1,n,1));
return 0;
}
P2339 [USACO04OPEN] Turning in Homework G
设状态 \(f[l][r][0/1]\) 表示对于区间l到r,站在左端点或右端点所需的最小时间。
但是这个状态时间维度又不好处理,因此考虑增加限制
状态 \(f[l][r][0/1]\) 代表除了l到r的区间以外,所有作业都交了且站在l或r上所需的最小时间(站在l或r上时这个点作业是交了的)
考虑转移。由于需要不重不漏地考虑完所有情况,所以对于每个状态,都需要从 \(l-1,l+1,r+1,r-1\) 四个端点共 12 种情况转移
但考虑到如果 \(f[l][r][0]\) 从 \(f[l\pm1][r][0/1]\) 上转移而来,那么会与 \(f[l-1][l][0]\) 到 \(f[l][l+1][1]\) 到 \(f[l-1][r][0]\) 等情况
重合,因此通篇考虑下来,就只剩了4种情况
f[l][r][0]=min(f[l][r][0],max(f[l-1][r][0]+a[l].x-a[l-1].x,a[l].t));
f[l][r][0]=min(f[l][r][0],max(f[l][r+1][1]+a[r+1].x-a[l].x,a[l].t));
f[l][r][1]=min(f[l][r][1],max(f[l][r+1][1]+a[r+1].x-a[r].x,a[r].t));
f[l][r][1]=min(f[l][r][1],max(f[l-1][r][0]+a[r].x-a[l-1].x,a[r].t));
- 上述4个式子有的看似并不优甚至不符合定义,如第4个。事实上,中间走过的所有情况都会被枚举到。因此,做DP题还是要考虑周到
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9+7;
const int N=1e3+6;
struct node
{
int x,t;
bool operator <(node &y)const{
return x<y.x;
}
}a[N];
int f[N][N][2];
int main()
{
ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);
int c,b,h;cin>>c>>h>>b;
for(int i=1;i<=c;i++) cin>>a[i].x>>a[i].t;
sort(a+1,a+c+1);
memset(f,0x3f3f,sizeof(f));
f[1][c][0]=max(a[1].x,a[1].t);
f[1][c][1]=max(a[c].x,a[c].t);
for(int l=1;l<=c;l++)
{
for(int r=c;r>=1;r--) //注意,由于所有答案都是由l-1与r+1得来的,因此l需要正着,r需要倒着枚举
{
f[l][r][0]=min(f[l][r][0],max(f[l-1][r][0]+a[l].x-a[l-1].x,a[l].t));
f[l][r][0]=min(f[l][r][0],max(f[l][r+1][1]+a[r+1].x-a[l].x,a[l].t));
f[l][r][1]=min(f[l][r][1],max(f[l][r+1][1]+a[r+1].x-a[r].x,a[r].t));
f[l][r][1]=min(f[l][r][1],max(f[l-1][r][0]+a[r].x-a[l-1].x,a[r].t));
}
}
int ans=inf;
for(int i=1;i<=c;i++)
{
ans=min({ans,f[i][i][0]+abs(a[i].x-b),f[i][i][1]+abs(a[i].x-b)});//处理每个点到终点的时间最小值
}
cout<<ans<<endl;
}
P4870 [BalticOI 2009 Day1] 甲虫
首先数据范围很小,而每个水珠的贡献一直在变化。
由于所有水珠的初始水量都相同,因此如果我们知道了经过的时间,那我们就可以统计每个水珠的贡献。
但是时间范围太大了,不好直接去放在状态里。那如何抛弃掉巨大的 \(m\) 与 \(x_i\) 让时间复杂度与 \(n\) 挂钩呢?
于是我们倒过来想,由于时间复杂度可以支持到 \(n^3\),因此我们直接去枚举到底要吃多少个水珠,去计算总共亏损了多少水量。
然后就是区间DP的套路了,设 \(f_{l,r,0/1}\) 表示吃完了 \(l\) 到 \(r\) 之间的水珠之后站在左或者右端点的亏损的最小值。
而状态转移就如下(这个应该得会吧):
f[l][r][0]=min({f[l][r][0],f[l+1][r][0]+(num-(r-l))*(loc[l+1]-loc[l]),f[l+1][r][1]+(num-(r-l))*(loc[r]-loc[l])});
f[l][r][1]=min({f[l][r][1],f[l][r-1][1]+(num-(r-l))*(loc[r]-loc[r-1]),f[l][r-1][0]+(num-(r-l))*(loc[r]-loc[l])});
表示的就是站在左边或者站在右边从左边或者右边走过来需要亏的水量。num指枚举的要吃水珠的数量。
由于当前这个暂时还没有吃(中间的路还要赶),因此还剩下 \(num-(r-l+1)+1\) 个水珠还没有吃,也就是还在晒着亏水量。
而答案就是
m*(r-l)-min(f[l][r][0],f[l][r][1])
取max。
注意这里假设水珠水量可以被晒成负数。能够这样处理的原因是如果水被晒成负数,那一定不优,因此不会选。
关于为何不是 \(m*(r-l+1)\) 的问题,应该不是因为所谓的 “因为还没喝掉要喝的水,所以减一”,而是因为在开头:
for(int i=1;i<=n;i++) cin>>loc[i],z=(loc[i]==0)||z;
if(!z) loc[++n]=0;
这是避免没有水滴在0处不好DP的问题。因此我们直接强制在0处加上一个水滴,然后这里就需要考虑到不能多算一个水滴的答案,就要减一。
但是如果本来有水滴在0处,就要把多减的加回去:
cout<<ans+z*m<<'\n';
因此如果你愿意也可以写成这样:
m*(r-l+1)-min(f[l][r][0],f[l][r][1])
cout<<(ans==0?0:(ans-(z^1)*m))<<'\n';
但很可惜本题并没有0处有水滴的情况,建议加一下hack。
时间复杂度 \(O(n^3)\)
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=605;
const int inf=1e18;
int n,m,loc[N],ans=0,f[N][N][2],pos;bool z=0;
signed main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>loc[i],z=(loc[i]==0);
if(!z) loc[++n]=0;
sort(loc+1,loc+n+1);pos=lower_bound(loc+1,loc+n+1,0)-loc;
for(int num=2;num<=n;num++)
{
memset(f,0x3f,sizeof(f));
f[pos][pos][0]=f[pos][pos][1]=0;
for(int len=2;len<=num;len++)
for(int l=1;l+len-1<=n;l++)
{
int r=l+len-1;
f[l][r][0]=min({f[l][r][0],f[l+1][r][0]+(num-(r-l))*(loc[l+1]-loc[l]),f[l+1][r][1]+(num-(r-l))*(loc[r]-loc[l])});
f[l][r][1]=min({f[l][r][1],f[l][r-1][1]+(num-(r-l))*(loc[r]-loc[r-1]),f[l][r-1][0]+(num-(r-l))*(loc[r]-loc[l])});
ans=max({ans,m*(r-l)-min(f[l][r][0],f[l][r][1])});
}
}
cout<<ans+z*m<<'\n';return 0;
}
然后最近写了几道单调性,想可不可以优化,发现还真行。
由于少喝水滴一定会劣,如果假定水滴可能减少为负数(就是本题的处理方法),再喝已经为负数的水滴也一定劣。
那么答案就一定会形成一个在吃的水滴数为x轴,最优答案为y轴的坐标系里的严格的上凸壳。
感觉已经够清楚了。然后就是找上凸壳的顶点,三分。
时间复杂度 \(O(n^2\log n)\) 然后就被 \(O(n^3)\) 吊打
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=605;
const int inf=1e18;
int n,m,loc[N],ans=0,f[N][N][2],pos;bool z=0;
int solve(int num)
{
int res=0;
memset(f,0x3f,sizeof(f));
f[pos][pos][0]=f[pos][pos][1]=0;
for(int len=2;len<=num;len++)
for(int l=1;l+len-1<=n;l++)
{
int r=l+len-1;
f[l][r][0]=min({f[l][r][0],f[l+1][r][0]+(num-(r-l))*(loc[l+1]-loc[l]),f[l+1][r][1]+(num-(r-l))*(loc[r]-loc[l])});
f[l][r][1]=min({f[l][r][1],f[l][r-1][1]+(num-(r-l))*(loc[r]-loc[r-1]),f[l][r-1][0]+(num-(r-l))*(loc[r]-loc[l])});
res=max({res,m*(r-l)-min(f[l][r][0],f[l][r][1])});
}ans=max(ans,res);
return res;
}
signed main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>loc[i],z=(loc[i]==0)||z;
if(!z) loc[++n]=0;
sort(loc+1,loc+n+1);pos=lower_bound(loc+1,loc+n+1,0)-loc;
int l=1,r=n;
while(l<r-1)
{
int mid=(l+r)>>1,mmid=(mid+r)>>1,resm=solve(mid),resmm=solve(mmid);
if(resm>resmm) r=mmid;else l=mid;
}
ans=max({ans,solve(l),solve(r)});
cout<<ans+z*m<<'\n';return 0;
}

浙公网安备 33010602011771号