松果
题目描述
有N棵松果树从左往右排一行,桃桃是一只松鼠,它现在在第一棵松果树上。它想吃尽量多的松果,但它不想在地上走,而只想从一棵树跳到另一棵树上。松鼠的体力有个上限,每次不能跳的太远,也不能跳太多次。每当它跳到一棵树上,就会把那棵树上的松果全部都吃了。它最多能吃到多少个松果?
输入格式 1725.in
第一行,三个整数:N、D、M。N表示松果树的数量,D表示松鼠每次跳跃的最大距离,M表示松鼠最多能跳跃M次。
接下来有N行,每行两个整数:Ai和Bi。其中Ai表示第i棵树上的松果的数量,Bi表示第i棵树与第1棵树的距离,其中B1保证是0。
数据保证这N棵树从左往右的次序给出,即Bi是递增的,不存在多棵树在同一地点。
输出格式 1725.out
一个整数。
输入样例 1725.in
5 5 2
6 0
8 3
4 5
6 7
9 10
输出样例 1725.out
20
【数据范围】
Ai <= 10000, D <= 10000
对于40%的数据,M < N <= 100, Bi<= 10000
对于100%的数据,M < N <= 2000,Bi <= 10000
松树需要从一棵树跳到另一棵树,则具有阶段性。有次数的限制,M,N较大,寻找最优解。不难看出,这是一道DP。
状态怎么设计?第一维需要记录跳到了哪棵树上。此外,由于跳的次数有限制,且影响结果,因此必须要记录次数这一维。
于是,我定义f[i][j]表示跳到了第i棵树上,在j棵树上停留过(跳过j-1次)的最优解。
子问题是什么?就是松树停留的前一棵树。因此, 状态转移方程就为:
F[i][j]=a[i]+max(f[k][j-1])(1<=k<i,且b[i]-b[k]<=d)
最终max(f[1][m+1],f[2][m+1]……f[n][m+1])即为答案。
但是我们发现,如果for循环枚举k,则时间复杂度为N^3级别的,会造成超时。因此需要更加高效的算法。
方法一:
不难发现,k这个参数的增长是连续的。查询一段的最值,可以用到线段树。此处用二维的线段树,其中第一维为次数,作为标识。
我们可以预处理出对于i而言,编号最小的mink。每次用线段树查询f[mink][j-1]~f[i-1][j-1]的最大值,代替上文的for循环,就能快速获知答案了。
时间复杂度:O(N^2*logN)
代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,d,m,ans=0;
int f[2005][2005],a[2005],b[2005];
int tree[2005][8005];
int findlast(int i)
{
int lo=0,hi=i;
while(lo+1<hi)
{
int mid=(lo+hi)/2;
if(b[mid]>=b[i]-d) hi=mid;
else lo=mid;
}
return hi;
}
int getans(int now,int l,int r,int s,int t,int num)
{
if(l>t||r<s) return -1;
if(s<=l&&r<=t) return tree[num][now];
int mid=(l+r)/2;
return max(getans(now*2,l,mid,s,t,num),getans(now*2+1,mid+1,r,s,t,num));
}
void update(int now,int l,int r,int num,int i,int v)
{
if(l>i||r<i) return;
if(l==i&&r==i)
{
tree[num][now]=v;
return;
}
int mid=(l+r)/2,ls=now*2,rs=now*2+1;
update(ls,l,mid,num,i,v);
update(rs,mid+1,r,num,i,v);
tree[num][now]=max(tree[num][ls],tree[num][rs]);
}
void dp(int i)
{
for(int pick=3;pick<=m+1;pick++)
{
if(i<pick) continue;
int now,last;
last=findlast(i);
if(last>=i) continue;
now=getans(1,1,n,last,i-1,pick-1);
if(now!=-1)
{
f[i][pick]=now+a[i];
update(1,1,n,pick,i,f[i][pick]);
}
}
}
int main()
{
cin>>n>>d>>m;
for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
memset(f,-1,sizeof(f));
memset(tree,-1,sizeof(tree));
f[1][1]=a[1];
update(1,1,n,1,1,a[1]);
for(int i=2;i<=n;i++)
{
if(b[i]<=d)
{
f[i][2]=a[1]+a[i];
update(1,1,n,2,i,f[i][2]);
}
dp(i);
}
for(int i=1;i<=n;i++) ans=max(ans,f[i][m+1]);
cout<<ans<<endl;
return 0;
}
方法二:
查询最值,我们同样可以用单调队列。此处也是要开二维,其中第一维为次数作为标识。
由于要求最大值,此处维护单调递减队列。另外,当队首元素不能够跳到第i棵树时,就将其出队。
值得注意的是,当求好f[i][j]之后,不要立即将其入队。因为后面求f[i][j+1]时,需要跳j次的最大值,这时就有可能取到f[i][j],造成了错误。应该在所有f[i][]求好了之后,在统一进行入队操作。
由于每一个f[i][j]在单调队列中,最多入队一次,出队一次,因此时间复杂度只是加上2*N^2。
时间复杂度:O(N^2)
代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=2005;
int n,d,m,ans=0;
int f[MAXN][MAXN],a[MAXN],b[MAXN];//跳到第i棵树,到过j棵树的最大能量值
int q[MAXN][MAXN];//第一维为标识。
int inde[MAXN][MAXN];
int head[MAXN],tail[MAXN];
void dp(int i)
{
for(int pi=2;pi<=min(i,m+1);pi++)
{
int now;
while( head[pi-1]<=tail[pi-1] && (b[i]-inde[pi-1][head[pi-1]])>d) head[pi-1]++;
if(head[pi-1]>tail[pi-1]) continue;//不存在。
f[i][pi]=q[pi-1][head[pi-1]]+a[i];
}
for(int pi=2;pi<=min(i,m+1);pi++)
{
int now=f[i][pi];
if(now==-1) continue;
while(head[pi]<=tail[pi]&&q[pi][tail[pi]]<=now) tail[pi]--;
q[pi][++tail[pi]]=now;
inde[pi][tail[pi]]=b[i];
}
}
int main()
{
cin>>n>>d>>m;
for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
for(int i=1;i<=n;i++) head[i]=1;
memset(f,-1,sizeof(f));
f[1][1]=a[1];
q[1][1]=a[1];
inde[1][1]=0;
tail[1]=1;
for(int i=2;i<=n;i++)
dp(i);
for(int i=1;i<=n;i++) ans=max(ans,f[i][m+1]);
cout<<ans<<endl;
return 0;
}