877. 石子游戏

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 。


##思考
####数学方法
  这道题当你仔细研究的是时候,你就会发现无论什么输入,最后都是亚历克斯赢。因为亚历克斯总是赢得2堆时的游戏,然而通过一些努力,我们可以知道亚历克斯也可以赢得4堆的情况;
  因为当亚历克斯总能选的1,3或者2,4中的任意一种情况,那么那个多就选那个,就必赢了。以此推导道N堆(N为偶数),那么亚历克斯能选到(1,3,5…N-1)或者(2,4,6…N),所以亚历克斯总是赢。

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

####动态规划
这里我们改一下规则,改成任意推数的石子,避免先手必应的局面。
  既然是动态规划,那我们要定义dp数组,然后和股票买卖系列问题类似,只要找到「状态」和「选择」,一切就水到渠成了。
###一、定义dp[]数组的含义
  定义 dp数组的含义是很有技术含量的,同一问题可能有多种定义方法,不同的定义会引出不同的状态转移方程,不过只要逻辑没有问题,最终都能得到相同的答案。
  介绍 dpdp 数组的含义之前,我们先看一下 dpdp 数组最终的样子:
s\e|0|1|2|3
–|--|–|--|
0|(3,0)|(9,3)| (4,9)|(11,4)
1||(9,0)|(9,1)|(10,2)
2|||(1,0)|(2s,1)
3||||(2,0)
下文讲解时,认为元组是包含 firstfirst 和 secondsecond 属性的一个类,而且为了节省篇幅,将这两个属性简写为 firfir 和 secsec。比如按上图的数据,我们说 dp[1][3].fir = 10,dp[0][1].sec = 3。

dp[i][j].fir 表示,对于 piles[i…j] 这部分石头堆,先手能获得的最高分数。
dp[i][j].sec 表示,对于piles[i…j] 这部分石头堆,后手能获得的最高分数。

举例理解一下,假设 piles = [3, 9, 1, 2],索引从 0 开始
dp[0][1].fir = 9 意味着:面对石头堆 [3,9],先手最终能够获得 9 分。
dp[1][3].sec = 2 意味着:面对石头堆 [9, 1, 2],后手最终能够获得 2 分。

  我们想求的答案是先手和后手最终分数之差,按照这个定义也就是 dp[0][n-1].fir - dp[0][n-1].sec,即面对整个 piles,先手的最优得分和后手的最优得分之差。
###二、状态转移方程
写状态转移方程很简单,首先要找到所有「状态」和每个状态可以做的「选择」,然后择优。
根据前面对 dp 数组的定义,状态显然有三个:开始的索引 i,结束的索引 j,当前轮到的人。

dp[i][j][fir or sec]
其中:
0 <= i < piles.length
i <= j < piles.length

  对于这个问题的每个状态,可以做的选择有两个:选择最左边的那堆石头,或者选择最右边的那堆石头。 我们可以这样穷举所有状态:

n = piles.length
for 0 <= i < n:
   for j <= i < n:
     for who in {fir, sec}:
       dp[i][j][who] = max(left, right)

这道题的难点在于,两人是交替进行选择的,也就是说先手的选择会对后手有影响,这怎么表达出来呢?
根据我们对 dp 数组的定义,很容易解决这个难点,写出状态转移方程:

dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 )
# 解释:我作为先手,面对 piles[i…j] 时,有两种选择:
# 要么我选择最左边的那一堆石头,然后面对 piles[i+1…j]
# 但是此时轮到对方,相当于我变成了后手;
# 要么我选择最右边的那一堆石头,然后面对 piles[i…j-1]
# 但是此时轮到对方,相当于我变成了后手。
if 先手选择左边:
   dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
   dp[i][j].sec = dp[i][j-1].fir
# 解释:我作为后手,要等先手先选择,有两种情况:
# 如果先手选择了最左边那堆,给我剩下了 piles[i+1…j]
# 此时轮到我,我变成了先手;
# 如果先手选择了最右边那堆,给我剩下了 piles[i…j-1]
# 此时轮到我,我变成了先手。

根据 dpdp 数组的定义,我们也可以找出 base case,也就是最简单的情况:

dp[i][j].fir = piles[i]
dp[i][j].sec = 0
其中 0 <= i == j < n
# 解释:i 和 j 相等就是说面前只有一堆石头 piles[i]
# 那么显然先手的得分为 piles[i]
# 后手没有石头拿了,得分为 0

这里需要注意一点,我们发现 base case 是斜着的,而且我们推算 dp[i][j] 时需要用到
所以说算法不能简单的一行一行遍历 dp 数组,而要斜着遍历数组:dp[i+1][j] 和 dp[i][j-1]:

###三、代码实现
如何实现这个 fir 和 sec 元组呢,你可以用 python,自带元组类型;或者使用 C++ 的 pair 容器;或者用一个三维数组 dp[n][n][2],最后一个维度就相当于元组;或者我们自己写一个 Pair 类:

java
int n = piles.length;
		int[][][] dp = new int[n][n][2];
		for (int i = 0; i < n; i++) {
			dp[i][i][0] = piles[i];
			dp[i][i][1] = 0;
		}
		for (int l = 2; l <= n; l++) {
			for (int i = 0; i < n-l; i++) {
				int j = l + i-1;
				int left = piles[i] + dp[i + 1][j][1];
				int right = piles[j] + dp[i][j - 1][1];
				if (left > right) {
					dp[i][j][0] = left;
					dp[i][j][1] = dp[i + 1][j][0];
				} else {
					dp[i][j][0] = right;
					dp[i][j][1] = dp[i][j - 1][0];
				}
			}
		}
		if (dp[0][n - 1][0] > dp[0][n - 1][1]) {
			return true;
		} else {
			return false;
		}

注意这里python返回的是分数之差。

python
    def stoneGame(piles):
    # /* 返回游戏最后先手和后手的得分之差 */
    col = len(piles)
    dp = [[[0, 0] for _ in range(col)] for _ in range(col)]
    for i in range(col):
        dp[i][i][0] = piles[i]

    # 斜着遍历数组
    for k in range(2, col+1):
        for i in range(0, col-1):
            j = k + i - 1
            if j == col:  # 防止访问溢出
                break
            # print(i, j)  # 遍历路径

            left = piles[i] + dp[i+1][j][1]
            right = piles[j] + dp[i][j-1][1]

            # 先手选择最左边或最右边的分数
            if left > right:
                dp[i][j][0] = left
                dp[i][j][1] = dp[i+1][j][0]

            else:
                dp[i][j][0] = right
                dp[i][j][1] = dp[i][j-1][0]

    res = dp[0][col-1]
    return res[0] - res[1]

作者:labuladong
链接:https://leetcode-cn.com/problems/stone-game/solution/jie-jue-bo-yi-wen-ti-de-dong-tai-gui-hua-tong-yong/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值