luogu P5858 「SWTR-03」Golden Sword

题面传送门
看到这道题,暴力应该是很好打的,爆搜出放一件物品前要拿走多少,然后就记忆化出 d p dp dp了:设 f i , j f_{i,j} fi,j为放到第 i i i个物品,锅内加上当前还有 j j j件物品的最大总和,那么状态转移方程应该是 f i , j = m a x ( f i , k + a i ∗ j ) f_{i,j}=max(f_{i,k}+a_i*j) fi,j=max(fi,k+aij),其中 k k k表示没有拿走 < s <s件物品时锅内剩余的物品数,那么根据定义就可以确定 k k k的范围: j − 1 ≤ k ≤ m i n ( m , s + j − 1 ) j-1\leq k\leq min(m,s+j-1) j1kmin(m,s+j1),其中 j − 1 j-1 j1表示什么都不拿,为什么要 − 1 -1 1呢,因为我们还要放进去一件物品;而 s + j − 1 s+j-1 s+j1表示拿出了最大上限 s s s件物品。在这道题目中从已有状态推过去状态会好推一点,如果从已有状态推未来状态就要考虑越界就会很烦。这样时间复杂度 O ( n m s ) O(nms) O(nms)
代码实现:

#include
#include
#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 85 85分,要在 P J PJ PJ考场上我就直接开下一题了,毕竟就 15 15 15分的事( z y q zyq zyq:我少 15 15 15分会怎么样你知道吗?)
然后我们要考虑优化
观察一下 d p dp dp方程式: f i , j = m a x ( f i , k + a i ∗ j ) f_{i,j}=max(f_{i,k}+a_i*j) fi,j=max(fi,k+aij),我们可以乱搞一下,变成: f i , j = m a x ( f i , k ) + a i ∗ j f_{i,j}=max(f_{i,k})+a_i*j fi,j=max(fi,k)+aij,因为 a i ∗ j a_i*j aij是固定的,然后由于 k k k的界限随 j j j而变化,我们想到求解状态的一重循环这是一个区间取最值的 s b sb sb操作,可以用 s t st st表/线段树/单调队列来求解,由于前两个都带 l o g log log,就用单调队列吧。
我们来分析一下单调队列的适用性,当推到 f i , j f_{i,j} fi,j时, f i − 1 , j f_{i-1,j} fi1,j入队,但同时这个状态所有待转移的状态都要已经入队过了(注意这个”过”字,单调队列不要求一定要留在队列里),而 k k k的取值范围是 j − 1 ≤ k ≤ m i n ( m , s + j − 1 ) j-1\leq k\leq min(m,s+j-1) j1kmin(m,s+j1),拆成 m a x ( f i − 1 , j − 1 , m a x ( f i − 1 , k ) ) + a i ∗ j max(f_{i-1,j-1},max(f_{i-1,k}))+a_i*j max(fi1,j1,max(fi1,k))+aij, j ≤ k ≤ m i n ( m , s + j − 1 ) j≤k≤min(m,s+j-1) jkmin(m,s+j1),这样反转一下j的循环枚举顺序就可以保证所有转移状态都入队了。
代码实现:

#include
#include
#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);
}

最后不得不吐槽一句:单调队列常数好大啊!

你可能感兴趣的:(洛谷)