luogu P5858 「SWTR-03」Golden Sword
题面传送门
看到这道题,暴力应该是很好打的,爆搜出放一件物品前要拿走多少,然后就记忆化出\(dp\)了:设\(f_{i,j}\)为放到第\(i\)个物品,锅内加上当前还有\(j\)件物品的最大总和,那么状态转移方程应该是\(f_{i,j}=max(f_{i,k}+a_i*j)\),其中\(k\)表示没有拿走\(<s\)件物品时锅内剩余的物品数,那么根据定义就可以确定\(k\)的范围:\(j-1\leq k\leq min(m,s+j-1)\),其中\(j-1\)表示什么都不拿,为什么要\(-1\)呢,因为我们还要放进去一件物品;而\(s+j-1\)表示拿出了最大上限\(s\)件物品。在这道题目中从已有状态推过去状态会好推一点,如果从已有状态推未来状态就要考虑越界就会很烦。这样时间复杂度\(O(nms)\)
代码实现:
#include<cstdio>
#include<cstring>
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
long long n,m,s,f[5539][5539],ans=-1e15,tot,pus,a[5539],now,last;
int main(){
register int i,j,k;
scanf("%lld%lld%lld",&n,&m,&s);
for(i=0;i<=n;i++){
for(j=0;j<=m;j++) f[i][j]=-1e15;
}
for(i=1;i<=n;i++)scanf("%lld",&a[i]);
f[0][0]=0;
for(i=1;i<=n;i++){
for(j=1;j<=min(m,i);j++){
for(k=j-1;k<=min(m,s+j-1);k++) f[i][j]=max(f[i-1][k]+j*a[i],f[i][j]);
}
}
for(i=0;i<=m;i++)ans=max(ans,f[n][i]);
printf("%lld\n",ans);
}
居然有\(85\)分,要在\(PJ\)考场上我就直接开下一题了,毕竟就\(15\)分的事(\(zyq\):我少\(15\)分会怎么样你知道吗?)
然后我们要考虑优化
观察一下\(dp\)方程式:\(f_{i,j}=max(f_{i,k}+a_i*j)\),我们可以乱搞一下,变成:\(f_{i,j}=max(f_{i,k})+a_i*j\),因为\(a_i*j\)是固定的,然后由于\(k\)的界限随\(j\)而变化,我们想到求解状态的一重循环这是一个区间取最值的\(sb\)操作,可以用\(st\)表/线段树/单调队列来求解,由于前两个都带\(log\),就用单调队列吧。
我们来分析一下单调队列的适用性,当推到\(f_{i,j}\)时,\(f_{i-1,j}\)入队,但同时这个状态所有待转移的状态都要已经入队过了(注意这个”过”字,单调队列不要求一定要留在队列里),而\(k\)的取值范围是\(j-1\leq k\leq min(m,s+j-1)\),拆成\(max(f_{i-1,j-1},max(f_{i-1,k}))+a_i*j\),\(j≤k≤min(m,s+j-1)\),这样反转一下j的循环枚举顺序就可以保证所有转移状态都入队了。
代码实现:
#include<cstdio>
#include<cstring>
#define max(a,b) ((a)>(b)?(a):(b))
#define min(a,b) ((a)<(b)?(a):(b))
long long n,m,s,f[5539][5539],ans=-1e15,tot,pus,a[5539],now,last,q[5539],head,tail;
int main() {
register long long i,j,k;
scanf("%lld%lld%lld",&n,&m,&s);
for(i=0; i<=n; i++) {
for(j=0; j<=m; j++) f[i][j]=-1e15;
}
for(i=1; i<=n; i++)scanf("%lld",&a[i]);
f[0][0]=0;
for(i=1; i<=n; i++) {
head=tail=0;
for(j=min(m,i); j>=1; j--) {
//for(k=j-1;k<=min(m,s+j-1);k++) f[i][j]=max(f[i-1][k],f[i][j]);
while(q[head+1]>s+j-1&&head!=tail) head++;
while(f[i-1][q[tail]]<f[i-1][j]&&head!=tail) tail--;
q[++tail]=j;
f[i][j]=max(f[i-1][q[head+1]],f[i-1][j-1]);
f[i][j]+=j*a[i];
}
}
for(i=0; i<=m; i++)ans=max(ans,f[n][i]);
printf("%lld\n",ans);
}
最后不得不吐槽一句:单调队列常数好大啊!