877. Stone Game

Alex and Lee play a game with piles of stones.  There are an even number of piles arranged in a row, and each pile has a positive integer number of stones piles[i].

The objective of the game is to end with the most stones.  The total number of stones is odd, so there are no ties.

Alex and Lee take turns, with Alex starting first.  Each turn, a player takes the entire pile of stones from either the beginning or the end of the row.  This continues until there are no more piles left, at which point the person with the most stones wins.

Assuming Alex and Lee play optimally, return True if and only if Alex wins the game.

Example 1:

Input: [5,3,4,5]
Output: true
Explanation:
Alex starts first, and can only take the first 5 or the last 5.
Say he takes the first 5, so that the row becomes [3, 4, 5].
If Lee takes 3, then the board is [4, 5], and Alex takes 5 to win with 10 points.
If Lee takes the last 5, then the board is [3, 4], and Alex takes 4 to win with 9 points.
This demonstrated that taking the first 5 was a winning move for Alex, so we return true.

Note:

  1. 2 <= piles.length <= 500
  2. piles.length is even.
  3. 1 <= piles[i] <= 500
  4. sum(piles) is odd.

这道题说是有偶数堆的石子,每堆的石子个数可能不同,但石子总数是奇数个。现在 Alex 和 Lee (不应该是 Alice 和 Bob 么??)两个人轮流选石子堆,规则是每次只能选开头和末尾中的一堆,最终获得石子总数多的人获胜。若 Alex 先选,两个人都会一直做最优选择,问我们最终 Alex 是否能获胜。博主最先想到的方法是像 Predict the Winner 中的那样,用个 player 变量来记录当前是哪个玩家在操作,若为0,表示 Alex 在选,那么他只有两种选择,要么拿首堆,要么拿尾堆,两种情况分别调用递归,两个递归函数只要有一个能返回 true,则表示 Alex 可以获胜,还需要用个变量 cur0 来记录当前 Alex 的石子总数。同理,若 Lee 在选,即 player 为1的时候,也是只有两种选择,分别调用递归,两个递归函数只要有一个能返回 true,则表示 Lee 可以获胜,用 cur1 来记录当前 Lee 的石子总数。需要注意的是,当首堆或尾堆被选走了后,我们需要标记,这里就有两种方法,一种是从原 piles 中删除选走的堆(或者是新建一个不包含选走堆的数组),但是这种方法会包括大量的拷贝运算,无法通过 OJ。另一种方法是用两个指针 left 和 right,分别指向首尾的位置。当选取了首堆时,则 left 自增1,若选了尾堆时,则 right 自减1。这样就不用执行删除操作,或是拷贝数组了,大大的提高了运行效率,参见代码如下:
解法一:

class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        return helper(piles, 0, 0, 0, (int)piles.size() - 1, 0);     
    }
    bool helper(vector<int>& piles, int cur0, int cur1, int left, int right, int player) {
        if (left > right) return cur0 > cur1;
        if (player == 0) {
            return helper(piles, cur0 + piles[left], cur1, left + 1, right, 1) || helper(piles, cur0 + piles[right], cur1, left + 1, right, 1);
        } else {
            return helper(piles, cur0, cur1 + piles[left], left, right - 1, 0) || helper(piles, cur0, cur1 + piles[right], left, right - 1, 0);
        }
    }
};

这道题也可以使用动态规划 Dynamic Programming 来做,由于玩家获胜的规则是拿到的石子数多,那么多的石子数就可以量化为 dp 值。所以我们用一个二维数组,其中 dp[i][j] 表示在区间 [i, j] 内 Alex 比 Lee 多拿的石子数,若为正数,说明 Alex 拿得多,若为负数,则表示 Lee 拿得多。则最终只要看 dp[0][n-1] 的值,若为正数,则 Alex 能获胜。现在就要找状态转移方程了,我们想,在区间 [i, j] 内要计算 Alex 比 Lee 多拿的石子数,在这个区间内,Alex 只能拿i或者j位置上的石子,那么当 Alex 拿了 piles[i] 的话,等于 Alex 多了 piles[i] 个石子,此时区间缩小成了 [i+1, j],此时应该 Lee 拿了,此时根据我们以往的 DP 经验,应该调用子区间的 dp 值,没错,但这里 dp[i+1][j] 表示是在区间 [i+1, j] 内 Alex 多拿的石子数,但是若区间 [i+1, j] 内 Lee 先拿的话,其多拿的石子数也应该是 dp[i+1][j],因为两个人都要最优化拿,那么 dp[i][j] 的值其实可以被 piles[i] - dp[i+1][j] 更新,因为 Alex 拿了 piles[i],减去 Lee 多出的 dp[i+1][j],就是区间 [i, j] 中 Alex 多拿的石子数。同理,假如 Alex 先拿 piles[j],那么就用 piles[j] - dp[i][j-1] 来更新 dp[i][j],则我们用二者的较大值来更新即可。注意开始的时候要把 dp[i][i] 都初始化为 piles[i],还需要注意的是,这里的更新顺序很重要,是从小区间开始更新,在之前那道 Burst Balloons,博主详细的讲了这种 dp 的更新顺序,可以去看看,参见代码如下:
解法二:

class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        int n = piles.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = 0; i < n; ++i) dp[i][i] = piles[i];
        for (int len = 1; len < n; ++len) {
            for (int i = 0; i < n - len; ++i) {
                int j = i + len;
                dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
            }
        }
        return dp[0][n - 1] > 0;
    }
};

其实这道题是一道脑筋急转弯题,跟之前那道 Nim Game 有些像。原因就在于题目中的一个条件,那就是总共有偶数堆,那么就是可以分为堆数相等的两堆,比如我们按奇偶分为两堆。题目还说了石子总数为奇数个,那么分出的这两堆的石子总数一定是不相等的,那么我们只要每次一直取石子总数多的奇数堆或者偶数堆,Alex 就一定可以躺赢,所以最叼的方法就是直接返回 true。我。。我。。我王司徒。。表示不服。。。我从未见过如此。。机智过人。。的解法~
解法三:

class Solution {
public:
    bool stoneGame(vector<int>& piles) {
        return true;
    }
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/877

类似题目:

Burst Balloons

Nim Game

Flip Game II

Guess Number Higher or Lower II

Predict the Winner

参考资料:

https://leetcode.com/problems/stone-game/

https://leetcode.com/problems/stone-game/discuss/154610/DP-or-Just-return-true

LeetCode All in One 题目讲解汇总(持续更新中…)


转载请注明来源于 Grandyang 的博客 (grandyang.com),欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 grandyang@qq.com

💰


微信打赏


Venmo 打赏

(欢迎加入博主的知识星球,博主将及时答疑解惑,并分享刷题经验与总结,试运营期间前五十位可享受半价优惠~)

×

Help us with donation