cf1430F(dp/贪心好题)
题意:
现在有n波怪物,每波怪物起讫时间为l[i]~r[i],有a[i]只怪物,你现在有一把枪,弹夹容量为k,你开枪不用时间,一枪一个小朋友。但是换弹夹需要1s,并且换弹夹的时候,如果弹夹里有子弹,会将子弹扔掉再塞满。你需要在每波怪物的时间限制内将其杀光,并且花费的子弹数最少(包括扔掉的子弹),问你最少需要花费多少子弹。
**方法一:dp **
显然是求最少扔掉的子弹。我们考虑,在中间扔掉一定是不优的,需要扔掉的情况只有当后面一轮做不完的时候才扔,容易想到 d p [ i ] [ 0 / 1 ] dp[i][0/1] dp[i][0/1]表示前 i i i轮做完,第 i i i轮是扔还是不扔,但我们发现我们无法记录上一个扔掉的子弹数,太大了
但我们发现 d p [ i ] [ 0 / 1 ] = m i n ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] ) + ( c o s t ( 第 二 维 度 = = 1 ) ) dp[i][0/1]=min(dp[i-1][0],dp[i-1][1])+(cost(第二维度==1)) dp[i][0/1]=min(dp[i−1][0],dp[i−1][1])+(cost(第二维度==1))
即阶段从 i − 1 i-1 i−1递推到 i i i可以化为由某个 j < i j<i j<i递推到 i i i,我们发现,换弹是关键点,使用 d p dp dp常用套路,枚举最后一个换弹的是哪个位置, d p [ i ] dp[i] dp[i]表示前 i i i个做完,第 i i i个做完后换弹的情况,枚举上一个最后换弹的地方,可以由某个 j j j转移过来,但是有两个需要注意的地方。
- r i 和 l i + 1 r_i和l_{i+1} ri和li+1是可能有交点的,假如此时时间重叠,我们是不能换弹的,因为 d p [ j ] dp[j] dp[j]此时表示打完换了弹,用 d p [ j ] dp[j] dp[j]更新 d p [ j + 1 ] dp[j+1] dp[j+1]的时候,时间是不对的,少了1,使得后面的花费减少,此外这时候时间重叠不进行主动换弹显然是更优的,所以此时更新这个状态会导致后面花费错误的减少,不更新不会更劣,所以更新这个状态无意义,不更新
- 可以先枚举 i i i,再枚举前面的 j j j,复杂度也是 O ( n 2 ) O(n^2) O(n2)的,但是写起来麻烦一点,这里直接枚举上一个换弹的地方依次向后转移写起来更方便。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
typedef long long ll;
ll dp[maxn];//前i个做完,第i个做完换了
int l[maxn],r[maxn],a[maxn],k,n;
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i)scanf("%d%d%d",&l[i],&r[i],&a[i]),dp[i]=1e18;
dp[0]=0;
for(int i=0;i<n;++i){
ll ans=dp[i],num=k;
for(int j=i+1;j<=n;++j){
ll time=(max(0ll,(a[j]-num))+k-1)/k;
if(l[j]+time>r[j])break;
ll cost=num+time*k-a[j];
ans+=a[j];
if(j==n)dp[j]=min(dp[j],ans);
else if(l[j]+time<l[j+1])//不更新这个状态
dp[j]=min(dp[j],ans+cost);
num=cost;
}
}
cout<<(dp[n]==1e18?-1:dp[n])<<"\n";
return 0;
}
启示:
1. d p [ i ] 由 d p [ i − 1 ] dp[i]由dp[i-1] dp[i]由dp[i−1]转移过来做不了的时候,可以观察由前面某个 j j j转移过来是否等价且能做
2.枚举最后一个xxx来划分 d p dp dp
3.形如 d p [ i ] = d p [ j ] + x x dp[i]=dp[j]+xx dp[i]=dp[j]+xx j < i j<i j<i的时候,可以尝试枚举前面顺序转移到后面
方法二:贪心(待补)