2018 ACM-ICPC EC final I.Misunderstood … Missing——倒序dp

题目链接:

Misunderstood … Missing

题意:

有两种值A,D,A代表攻击一次怪兽能对怪兽造成的伤害。D代表每回合开始时A的增量。初始值均为0

给出三种操作,求使用这三种操作在n回合后可以达到的对怪兽伤害的最大值:

1.攻击怪兽,造成A+a[i]伤害。

2.不攻击怪兽,但使D增加b[i]。

3.不攻击怪兽,但使A增加c[i]。

输入:

样例数T

每组样例一个n代表回合

随后n行每行三个数ai bi ci

输出:

对于每组样例,输出可以对怪兽造成的最大伤害。

思路:

现场赛自闭题,a b c的值都太大了,以至于不知道该怎么dp。隐隐约约有一种感觉,知道应该按次数dp,也知道应该是倒序,但是dp数组的维度还是没想明白。

言归正传,由于a,b,c都太大,所以我们不能选其值作为dp的维度,因为根本开不下。那还有什么可以dp的东西呢?次数。

题目说了回合最多100次,100次要么选1,要么选2,要么选3,而选2和3本质是相同的,只要知道选2或者3之后哪几个回合攻击了,就可以知道选2或者3的收益。但是,“哪几个回合攻击”蕴含的信息量太大,它本身是一个集合,对于一个状态,我们不能存储一个集合,那么就该考虑如何把这个信息转化。

选3的时候,没必要知道具体是哪些回合攻击了,只需要知道选3之后攻击了多少次,就可以得出选3的收益——设选3之后攻击力j次,则收益为c[i]*j

选2的时候就比较麻烦了,看似我们必须知道哪些回合攻击了,才能知道选2的收益有多大。设x为i回合选2之后选择攻击的回合,把收益列出来:

b[i]*((x1-i) + (x2-i) + (x3-i) + ... + (xj - i)) = b[i]*(Σx - i*j)

发现,并不需要记录攻击回合的集合,只需要知道选择攻击的回合之和即可。

因此,dp数组选取三维数组:dp[i][j][k]:[第i回合][第i回合到第n回合选择攻击的次数][攻击的回合之和] = i到n回合对怪兽造成的伤害

状态转移方程:

dp[i][j+1][k+i] = max(dp[i][j+1][k+i],dp[i+1][j][k] + a[i])//选不选操作1
dp[i][j][k] = max(dp[i][j][k],dp[i+1][j][k] + max(j*c[i],(k-i*j)*b[i]))//选不选操作2或3

由于最后一次肯定要选1,所以dp[n][1][n] = a[n],然后从n-1开始,dp方向为倒序,直到回合1,在回合1里找最大的ans即可。

代码:

/*实现为了节省空间用了滚动数组*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
static const int maxn = 100010;
static const int INF = 0x3f3f3f3f;
static const int mod = (int)1e9 + 7;
static const double eps = 1e-6;
static const double pi = acos(-1);
 
void redirect(){
    #ifdef LOCAL
        freopen("test.txt","r",stdin);
    #endif
}
ll a[110],b[110],c[110];
ll dp[2][110][5060];//1-100之和为5050
int main(){
    redirect();
    int T,n;
    scanf("%d",&T);
    while(T--){
        ll ans = 0;
        memset(dp,0,sizeof(dp));
        scanf("%d",&n);
        for(int i = 1;i <= n;i++)
        scanf("%lld %lld %lld",&a[i],&b[i],&c[i]);
        dp[n%2][1][n] = a[n];//最后一次一定选择攻击,这里是贪心。
        for(int i = n-1;i > 0;i--){//从第n-1回合到1回合
            for(int j = 1;j <= n-i;j++)//j的含义为——选择操作前已选攻击的次数。这里正序倒序无关紧要,因为状态转移方程只对不同回合数的量转移。
            for(int k = (i+i+j-2)*(j-1)/2 + n;k <= (n-j+1+n)*j/2;k++){//枚举攻击回合之和,这里正序倒序也无关紧要。
                //这里k的最小值和最大值来源于对攻击回合的选取。
                //当从i开始选,连续j-1回合攻击,第n回合攻击的情况是回合之和最小的情况。
                //从n开始,倒着数j次攻击是回合数最大的情况。
                dp[i%2][j+1][k+i] = max(dp[i%2][j+1][k+i],dp[(i+1)%2][j][k] + a[i]);
                dp[i%2][j][k] = max(dp[i%2][j][k],dp[(i+1)%2][j][k] + max(j*c[i],(k-i*j)*b[i]));
            }
            memset(dp[(i+1)%2],0,sizeof(dp[(i+1)%2]));//滚动数组,故清零
        }
        //i=1时选dp的最大值为答案。
        for(int j = 1;j <= n;j++)
        for(int k = j*(j-1)/2 + n;k <= (n-j+1+n)*j/2;k++)
        ans = max(ans,dp[1][j][k]);
        printf("%lld\n",ans);
    }
    return 0;
}

总结:

思路不够开拓,难点在将集合转化成一个回合之和那里。以后碰到dp正序逆序都要想想,有什么不同,某个状态的一个集合能不能用一个数替代。

猜你喜欢

转载自blog.csdn.net/krypton12138/article/details/86422784