USACO2010 December Treasure Chest & POI2010 The Minima Game


我覺得我自己真是沒救了,簡單的yy題和dp都不會,心疼。

拿老師的話安慰一下自己:400Byte的dp不會寫,常見。(這TM是嘲諷吧

更兩道類似的 非常簡單的 dp題。


USACO2010 Treasure Chest

題目大意

小C和小Q在玩一個取數字的遊戲,給定一串數列,兩個人依次從數列的頭部或者尾部取走一個數,把每個人取到的數字相加,誰大就誰贏了。小C和小Q都非常聰明,他們每次會選擇對自己最優的取法。求最終他們的得分分別是多少。

Input

  • Line 1:有一個正整數N (N5000)
  • Line 2:一個長度爲N 的序列a (ai10000)
  • 【內存範圍】32MB。

Output

  • Line 2:兩個整數,分別表示小C和小Q的最終得分(小Q先取)。

Sample Input

4
1 3 2 4

Sample Output

3 7


Solution

題目中有一句非常玄學的話:小C和小Q都非常聰明,他們每次會選擇對自己最優的取法(或者說“從最優情況考慮”等)。什麼纔是最優的取法?也就是說,對於當前的殘局[i,j] ,當前player:

  • 選擇ai + 接下來殘局[i+1,j] 他能取到的最優解
  • 選擇aj + 接下來殘局[i,j1] 他能取到的最優解

於是我們定義dp[i][j] 表示在殘局[i,j] 某位player能取到的最優解。這裏我們需要解決下面這個問題:

不必對於當前局面,再分開討論player1,2的情況。

  • 由於每個player在他當前輪的時候必須要取掉一個數字,那麼每個player對應的每個局面一定都是偶數/奇數長度的區間。而對於相同高度處的遞歸解答樹,對應的所有可能局面的區間長度都是相同的。(所以本題經過博弈處理,變成了區間型dp)
  • 對於每個player,他們更新的方式是相同的。首先[i,j][i+1,j],[i,j1] 分別位於不同層,即dp[i][j]dp[i+1][j],dp[i][j1] 分別對應兩個player的最優解。由於[i,j] 區間內可以取到的元素總和是相同的。那麼:

    • 爲了[i,j] 取到最優解,我們需要殘局中對手的最優解最小

    這句話就是這道題的玄學所在了,也就是每個player的最優取法。類似的,帶博弈色彩的dp題需要掌握住下面幾個要素:

    • dp的定義要抓住“最優解”,即直接定義dp爲當前狀態下能取到的最優解即可。
    • 在考慮轉移的時候要抓住“自己的最優解”“對手的最優解”,即對手的最優解在什麼情況下才會直接更新到自己的最優解。

根據上述內容,我們可以列出轉移方程式:

dp[i,j]=t=ijA[t]min{dp[i+1,j],dp[i,j1]}
#include<bits/stdc++.h>
using namespace std;
#define M 5005
int Dp[M][M];
int sum[M];
int dp(int L,int R){
    if(!Dp[L][R])return Dp[L][R];
    return Dp[L][R]=sum[R]-sum[L-1]-min(dp(L+1,R),dp(L,R-1));
}//記憶化搜索
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1,x;i<=n;i++){
        scanf("%d",&x);
        Dp[i][i]=x;
        sum[i]=sum[i-1]+x;
    }
    int ans=dp(1,n);
    printf("%d %d\n",sum[n]-ans,ans);
    return 0;
}

因爲卡空間複雜度,所以接下來向正常dp優化接軌,有兩種優化方法:

  1. 從上述轉移方程式中,我們發現i 需要從i+1 轉移過來,而j 需要從j1 轉移過來,那我們讓兩位的for都按照這個轉移順序,外層的有效更新實際上就只介於i,i+1 兩層之間了。

  2. 從上述轉移方程式中,我們發現對於當前區間長度爲len 的一層,需要從所有len1 那一層更新過來。那麼我們可以定義dp[len,L] 表示長度爲len 的,左端點爲L 的區間的最優解。

兩種方法都可以直接套用滾動數組優化空間複雜度。

#include<bits/stdc++.h>
using namespace std;
#define M 5005
int Dp[2][M],sum[M]; 
int main(){
    int n,x;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&x);
        sum[i]=sum[i-1]+x;
    }
    for(int L=n;L;L--){
        int cur=L&1;
        for(int R=L;R<=n;R++)
            if(L==R)Dp[cur][R]=sum[R]-sum[L-1];
            else Dp[cur][R]=sum[R]-sum[L-1]-min(Dp[!cur][R],Dp[cur][R-1]);
    }
    printf("%d %d\n",sum[n]-Dp[1][n],Dp[1][n]);
    return 0;
}

提一句,開頭的兩條定義可以寫成更加直觀的形式:

dp[i,j]=maxsum[i+1,j]dp[i+1,j]+a[i]sum[i,j1]dp[i,j1]+a[j]
這樣轉移更加符合原意。提出相同的sum[i,j] 就變成前文提到的形式了。

POI2010 題解整理

POI2010 The Minima Game

Description

給出N個正整數,AB兩個人輪流取數,A先取。每次可以取任意多個數,直到N個數都被取走。每次獲得的得分爲取的數中的最小值,A和B的策略都是儘可能使得自己的得分減去對手的得分更大。在這樣的情況下,最終A的得分減去B的得分爲多少。

Input

  • Line 1:一個正整數N (N<=1,000,000)
  • Line 2:N個正整數(不超過109 )。

Output

  • 一個正整數,表示最終A與B的分差。

Sample Input

3
1 3 1

Sample Output

2


Solution

16/10/20更新:之前題解寫的雲裏霧裏,所以回爐重造掉了。

首先理清本題思路,題目中每位玩家在當前輪所得的分數是當前所取數集的最小值,由於原先是無序序列,所以我們考慮讓數組有序。還有一個顯然的貪心是A一定會取最大的元素,否則被B取掉之後就不優了。

那麼根據上述內容,當數組有序後,最後的情況一定是AB兩人將有序序列分成了多個區間,且A取最後一個區間。

按照上一道題目所講,我們定義dp[i] 表示當前player相對於對手的最大差值。按照兩人的思路,顯然都先從最大到小依次嘗試能否取過來。轉移條件:dp[i] 應該從另外一位player的最大的最優解更新過來。

  • dp[i] 只會影響[i+1,n]的值,而不會對前面的值產生影響(因爲前面的值是由另外一位玩家選擇的)。所以它要從前面的dp[j] 更新過來。
  • 取最大的最優解是因爲上面這句話,由於兩人都從最優情況考慮,所以對於另外一個玩家,他一定會取令當前玩家儘可能不優的,即對於他自己最優的轉移作爲他這一輪的選擇,於是當前玩家無奈只好繼承這個狀態,再儘可能讓自己優。

於是根據上述內容,我們可以得到下述暴力O(N2) 代碼(POI上有40整組分):

#include <bits/stdc++.h>
#define M 1000005
using namespace std;
int n,a[M];
long long dp[M];
long long dfs(int pos){
    long long &tmp=dp[pos];
    if(~tmp)return tmp;
    tmp=0;
    for(int j=pos-1;j>=1;j--)tmp=max(tmp,dfs(j));
    return tmp=a[pos]-tmp;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    sort(a+1,a+n+1);
    memset(dp,-1,sizeof(dp));
    long long ans=0;
    for(int i=n;i>=1;i--)
        ans=max(ans,dfs(i));//A取[i,n]這段區間的最優解
    cout<<ans<<endl;
}

按照遞推順序,我們需要從小到大依次更新過來才能得到之前的最優解,而且我們收集的是前面出現過的最大最優解,所以這個東西還可以一直順序維護下去,避免了O(N) 轉移。於是我們就可以得到300+B的O(N) 算法(綜上,本題經過博弈處理,變成簡單的線性dp):

#include <bits/stdc++.h>
#define M 1000005
using namespace std;
int a[M];
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    sort(a+1,a+n+1);
    long long dp=a[1];
    for(int i=2;i<=n;i++)
        if(dp<a[i]-dp)dp=a[i]-dp;
    cout<<dp<<endl;
}
發佈了48 篇原創文章 · 獲贊 3 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章