石子游戏系列【博弈论+动态规划】

文章目录

  • Leetcode 877.石子游戏
    • 问题描述
    • 解题报告
    • 实现代码
  • Leetcode 1140. 石子游戏 II
    • 问题描述
    • 解题报告
    • 实现代码
  • Leetcode 1406. 石子游戏 III
    • 问题描述
    • 解题报告
    • 实现代码
  • 总结
  • 参考资料

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} 1

解题报告

dp[i][j] 表示 [i~j] 这个区间取石子,先手
这个石子游戏取石子有两个方向,所以我们需要在一个区间内进行分析。 2 ^{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} 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} 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} 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 [蕴含 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} 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 来区分前后手;而第二题和第三题均是“为了使得先手得到最优值,后手接下来走的一步在某个范围是最差的”

参考资料

[1] Leetcode 877.石子游戏
[2] Leetcode 877 题解区:labuladong
[3] Leetcode 1140. 石子游戏 II
[4]Leetcode 1140 题解区:zyh518
[5] Leetcode 1406. 石子游戏 III

你可能感兴趣的:(leetcode,动态规划,博弈论)