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正序逆序都要想想,有什麼不同,某個狀態的一個集合能不能用一個數替代。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章