石子游戲系列【博弈論+動態規劃】

Leetcode 877.石子游戲

問題描述

亞歷克斯和李用幾堆石子在做遊戲。偶數堆石子排成一行,每堆都有正整數顆石子 piles[i]

遊戲以誰手中的石子最多來決出勝負。石子的總數是奇數,所以沒有平局。

亞歷克斯和李輪流進行,亞歷克斯先開始。 每回合,玩家從行的開始或結束處取走整堆石頭。 這種情況一直持續到沒有更多的石子堆爲止,此時手中石子最多的玩家獲勝。

假設亞歷克斯和李都發揮出最佳水平,當亞歷克斯贏得比賽時返回 true ,當李贏得比賽時返回 false

示例:

輸入:[5,3,4,5]
輸出:true
解釋:
亞歷克斯先開始,只能拿前 5 顆或後 5 顆石子 。
假設他取了前 5 顆,這一行就變成了 [3,4,5] 。
如果李拿走前 3 顆,那麼剩下的是 [4,5],亞歷克斯拿走後 5 顆贏得 10 分。
如果李拿走後 5 顆,那麼剩下的是 [3,4],亞歷克斯拿走後 4 顆贏得 9 分。
這表明,取前 5 顆石子對亞歷克斯來說是一個勝利的舉動,所以我們返回 true 。1^{1}

解題報告

dp[i][j] 表示 [i~j] 這個區間取石子,先手
這個石子游戲取石子有兩個方向,所以我們需要在一個區間內進行分析。2^{2}

實現代碼

class Solution{
    public:
        bool stoneGame(vector<int>&piles){
            int len=piles.size(),j,sum=0;
            vector<vector<pair<int,int>>>dp(len,vector<pair<int, int>>(len, {0,0}));

            for(int i=0;i<len;i++){
                dp[i][i].first=piles[i];
                sum+=piles[i];
            }

            for(int dis=1;dis<len;dis++){
                for(int i=0;i<len&&i+dis<len;i++){
                    j=i+dis;
                    int left = piles[i] + dp[i+1][j].second;
                    int right = piles[j] + dp[i][j-1].second;
                    // 套用狀態轉移方程
                    if (left > right) {
                        dp[i][j].first = left;
                        dp[i][j].second = dp[i+1][j].first;
                    } 
                    else {
                        dp[i][j].first = right;
                        dp[i][j].second = dp[i][j-1].first;
                    }
                }
            }
            return dp[0][len-1].first>dp[0][len-1].second;
        }
};

2^{2}

Leetcode 1140. 石子游戲 II

問題描述

亞歷克斯和李繼續他們的石子游戲。許多堆石子 排成一行,每堆都有正整數顆石子 piles[i]。遊戲以誰手中的石子最多來決出勝負。

亞歷克斯和李輪流進行,亞歷克斯先開始。最初,M = 1

在每個玩家的回合中,該玩家可以拿走剩下的 前 X 堆的所有石子,其中 1 <= X <= 2M。然後,令 M = max(M, X)

遊戲一直持續到所有石子都被拿走。

假設亞歷克斯和李都發揮出最佳水平,返回亞歷克斯可以得到的最大數量的石頭。

示例:

輸入:piles = [2,7,9,4,4]
輸出:10
解釋
如果亞歷克斯在開始時拿走一堆石子,李拿走兩堆,接着亞歷克斯也拿走兩堆。在這種情況下,亞歷克斯可以拿到 2 + 4 + 4 = 10 顆石子。
如果亞歷克斯在開始時拿走兩堆石子,那麼李就可以拿走剩下全部三堆石子。在這種情況下,亞歷克斯可以拿到 2 + 7 = 9 顆石子。
所以我們返回更大的 10。

提示:

1 <= piles.length <= 100
1 <= piles[i] <= 10 ^ 4

3^{3}

解題報告

dp[i][j] 表示剩餘[i : len - 1]堆時,M = j的情況下,先取的人能獲得的最多石子數。

轉移方程爲:

  • i + 2M >= len 時, dp[i][M] = sum[i : len - 1], 剩下的堆數能夠直接全部取走,那麼最優的情況就是剩下的石子總和
  • i + 2M < len 時, dp[i][M] = max(dp[i][M], sum[i : len - 1] - dp[i + x][max(M, x)]), 其中 1 <= x <= 2M,剩下的堆數不能全部取走,那麼最優情況就是讓下一個人取的更少。對於我所有的 x 取值,下一個人從 x 開始取起,M 變爲 max(M, x),所以下一個人能取 dp[i + x][max(M, x)],我最多能取 sum[i : len - 1] - dp[i + x][max(M, x)]

實現時,需要兩重循環來計算 dp[i][j], 其中計算 dp[i][j] 具體的值時,還需要一重循環枚舉當前先手取多少堆石子,所以一共是三重循環。
其中 i 的取值範圍是 len-1~0j 的取值範圍是 1~len/2+1,因爲我們在計算 dp[i][j] 時是通過 dp[i+x][max(M, x)] 轉化來的,而 i<=2*M,所以 i+2*M 必定是小於 len,所以 M 必定是小於 len/2 的。4^{4}

實現代碼

class Solution {
public:
    int stoneGameII(vector<int>& piles) {
        int len = piles.size();
        int sum = 0; 
        // dp[i][j]表示當前是第i波,m=j;
        vector<vector<int>>dp(len, vector<int>(len, 0));
        for(int i = len - 1; i >= 0; i--){
            sum += piles[i];// 表示當前所剩下的所有棋子的和
            for(int M = 1; M <len/2+1; M++){
                if(i + 2 * M-1 >=len-1){
                    dp[i][M] = sum;
                    continue;
                }
                for(int x=1;x<=2*M&&i+x<len;x++){
                    dp[i][M] = max(dp[i][M], sum - dp[i + x][max(M, x)]);
                }
            }
        }
        return dp[0][1];
    }
};

這裏有一個疑問??

for(int x=1;x<=2*M&&i+x<len;x++){
    dp[i][M] = max(dp[i][M], sum - dp[i + x][max(M, x)]);
}

爲什麼 i+x<len [蘊含 dp 的第一維大小爲 len]可以,i+x<=len [蘊含 dp 的第二維大小爲 len+1] 也可以呢??

Leetcode 1406. 石子游戲 III

問題描述

Alice 和 Bob 用幾堆石子在做遊戲。幾堆石子排成一行,每堆石子都對應一個得分,由數組 stoneValue 給出。

Alice 和 Bob 輪流取石子,Alice 總是先開始。在每個玩家的回合中,該玩家可以拿走剩下石子中的的前 1、2 或 3 堆石子 。比賽一直持續到所有石頭都被拿走。

每個玩家的最終得分爲他所拿到的每堆石子的對應得分之和。每個玩家的初始分數都是 0 。比賽的目標是決出最高分,得分最高的選手將會贏得比賽,比賽也可能會出現平局。

假設 Alice 和 Bob 都採取 最優策略 。如果 Alice 贏了就返回 “Alice” ,Bob 贏了就返回 “Bob”,平局(分數相同)返回 “Tie” 。

示例 1:

輸入:values = [1,2,3,7]
輸出:“Bob”
解釋:Alice 總是會輸,她的最佳選擇是拿走前三堆,得分變成 6 。但是 Bob 的得分爲 7,Bob 獲勝。

提示:

  • 1 <= values.length <= 50000
  • -1000 <= values[i] <= 1000

5^{5}

解題報告

dp[i] 表示剩餘[i : len - 1]堆時,先取的人能獲得的最多石子數。
很明顯,dp[i]=max(sum-dp[i+1], sum-dp[i+2], sum-dp[i-3])

實現代碼

錯誤實現:

class Solution {
public:
    string stoneGameIII(vector<int>& stoneValue) {
        int len=stoneValue.size(),sum=stoneValue[len-1];
        vector<int>dp(len, 0);
        dp[len-1]=stoneValue[len-1];
        for(int i=len-2;i>=0;i--){
            dp[i]=INT_MIN;
            sum+=stoneValue[i];
            if(i+2>=len-1) dp[i]=sum;
            else {
                for(int j=1;j<=3&&j+i<len;j++){
                    dp[i]=max(dp[i], sum-dp[i+j]);
                }
            }
        }
        if(dp[0]>sum-dp[0]) return "Alice";
        else if(dp[0]<sum-dp[0]) return "Bob";
        else return "Tie";
    }
};

這種實現完全模仿第二題的,但是這道題 -1000 <= values[i] <= 1000,當 i+3>=len 時,不是後面的石子均取走。
由於石子堆中有負數,所以 dp[] 初始化 INT_MIN
因爲 dp[] 初始化 INT_MIN,所以 dp[] 的大小爲 len+1,因爲最後只剩下 [1,3] 堆石子時,可以一次性取完

class Solution {
public:
    string stoneGameIII(vector<int>& stoneValue) {
        int len=stoneValue.size(),sum=stoneValue[len-1];
        vector<int>dp(len+1, 0);
        dp[len-1]=stoneValue[len-1];
        for(int i=len-2;i>=0;i--){
            dp[i]=INT_MIN;
            sum+=stoneValue[i];
            for(int j=1;j<=3&&j+i<=len;j++){
                dp[i]=max(dp[i], sum-dp[i+j]);
            }
        }
        if(dp[0]>sum-dp[0]) return "Alice";
        else if(dp[0]<sum-dp[0]) return "Bob";
        else return "Tie";
    }
};
for(int j=1;j<=3&&j+i<=len;j++){
   dp[i]=max(dp[i], sum-dp[i+j]);
}

這裏,j=1 表示當前先手只取一堆石頭 stoneValue[i],後手從 stoneValue[] 的第 i+1 堆石頭開始取。

總結

  • 第一個題目可以在兩個方向取石子,所以需要兩維的數組來分析取物。
    第二個和第三個題目只能在一個方向取石子,只要一維就可以分析取物,但是第二題取石子的堆數是不確定的,而且取石子的堆數在較大的範圍內變動,所以需要第二維來分析取多少堆石子。第三題取石子的堆數也是不確定的,但是隻有三個取值,所以一維數組就可以解決問題。
  • 這種博弈題目,在轉移方程中需要體現博弈的過程,第一題通過給 dp 的每個元素設置成 pair<int, int> 來區分前後手;而第二題和第三題均是“爲了使得先手得到最優值,後手接下來走的一步在某個範圍是最差的”

參考資料

[1] Leetcode 877.石子游戲
[2] Leetcode 877 題解區:labuladong
[3] Leetcode 1140. 石子游戲 II
[4]Leetcode 1140 題解區:zyh518
[5] Leetcode 1406. 石子游戲 III

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