【算法修炼道路】石子游戏

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/stone-game
 

题目:甲、乙两人用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 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 。


关键字:【动态规划】【博弈】【极小化极大】

答案:return true(即先手的始终胜利)

思路:枚举是当然可以的,但是这肯定不是我们想要的。

这道题的我理解是每一步只考虑当前挑选石子的人的最优解。

比如:

当石子只剩下[4,5]的时候,此时(甲)的最优解拿5,记为dp甲[4,5]=[5],dp乙[4,5]=[4];

当石子只剩下[3,4]的时候,此时(甲)的最优解拿4,记为dp甲[3,4]=[4],dp乙[3,4]=[3];

当石子只剩下[3,4,5]的时候,此时(乙)的最优解是拿5、3,记为dp乙[3,4,5]=[5,3],dp甲[3,4,5]=[4];

【这里我们只考虑当前场景下拿石子的人,而不去考虑当前是谁去拿这个石子。】

  • 前两步都很好理解,因为只剩下两个石子,所以直接拿大的那个即可,即  if   a > b  then  a    else    b
  • 第三步是怎么来的呢:因为当前选手拿石子只能从开头或者结尾去拿,所以当前选选手(乙)只有两个选择,即要么拿3,要么拿4。
    • 如果(乙)拿3,那么(甲)肯定拿5,因为(甲)必须保证自己发挥最佳水平,最后(乙)拿走最后剩下的4;
    • 如果(乙)拿5,那么(甲)肯定拿4,因为(甲)必须保证自己发挥最佳水平,最后(乙)拿走最后剩下的3;;
    • 当前选手(乙)的两种选择已经有了:

dp乙[3,4,5]=[3,4]    ,   dp甲[3,4,5]=[5]

dp乙[3,4,5]=[5,3]    ,   dp甲[3,4,5]=[4]

这两种情况肯定是第二种更好,因为5+3-4>3+4-5,第二种情况乙能比甲多4个石子,明显好于第一种情况,因此当前选手(乙)的最优解是[5,3],即dp乙[3,4,5]=[5,3]


1)我们不妨改变一下上面的写法:

当石子只剩下[4,5]的时候,此时(甲)的最优解拿5,记为dp甲[4,5]=5,dp乙[4,5]=4;

当石子只剩下[3,4]的时候,此时(甲)的最优解拿4,记为dp甲[3,4]=4,dp乙[3,4]=3;

当石子只剩下[3,4,5]的时候,此时(乙)的最优解是拿5、3,记为dp乙[3,4,5]=8,dp甲[3,4,5]=4;

等式右边表示选手手中石子的个数。


2)我们可以再改变一下写法:

当石子只剩下[4,5]的时候,此时(甲)的最优解拿5,记为dp甲[2,3]=5,dp乙[2,3]=4;

当石子只剩下[3,4]的时候,此时(甲)的最优解拿4,记为dp甲[1,2]=4,dp乙[1,2]=3;

当石子只剩下[3,4,5]的时候,此时(乙)的最优解是拿5、3,记为dp乙[1,3]=8,dp甲[1,3]=4;

等式左边中括号里面是对应石子在数组中的索引,这样子,矩阵模型就出来了。


3)我们最后再优化一下这个矩阵:

当石子只剩下[4,5]的时候,dp[2,3]=1;

当石子只剩下[3,4]的时候,dp[1,2]=1;

当石子只剩下[3,4,5]的时候,dp[1,3]=4;

dp[i][j]表示场上剩下第i-j堆石子的时候,当前选手最多能比对手多出几个石子(最优解)。

dp[0][石子堆数-1] 即表示从第0堆石子一直到最后一堆石子的时候,甲比乙最多能多出多少个石子,即全盘最优解。


当i=j的时候:

dp[ i ] [ j ] = piles[ i ],(场上仅剩一堆石子的时候最优解就为这堆石子包含的石头的个数)

当i !=j的时候:

dp[ i ] [ j ] = Max (piles[ i ] - ( dp[ i+1 ] [ j ] )    ,     piles[ j ] - ( dp[ i ] [ j-1 ] ))

从两端选石子的时候,

假如选左端的石子堆,piles[i] - (dp[i+1][j]) 表示第i堆石子减去右边剩下石子堆的对手的最优解所对应的石子个数

假如选右端的石子堆,piles[j] - (dp[i][j-1] ) 表示第j堆石子减去左边剩下石子堆的对手的最优解所对应的石子个数

 

这样动态方程就出来了(我觉得这样思路也很清晰了)


代码:

class Solution {
public:
    bool stoneGame(vector<int>& piles) {

        // 如果是2个石子堆,那么先手的人必胜
        if (piles.size() == 2) return true;

        // 初始化dp矩阵
        int dp[piles.size() + 1][piles.size() + 1];

        // 初始化矩阵对角线,对应表示仅剩一个石子的时候,最优解就为对应的石子堆包含石子的个数
        for (int i=0; i<piles.size(); ++i) {
            dp[i][i] = piles[i];
        }

        // 从后往前遍历,求出每一个格子的值
        for (int i=piles.size() - 1; i>=0; --i) {
            for (int j=i+1; j<piles.size(); ++j) {
                int leftMax = piles[i] - dp[i+1][j];
                int rightMax = piles[j] - dp[i][j-1];
                dp[i][j] = (leftMax > rightMax) ? leftMax : rightMax;
            }
        }

        // 如果最右上角的那个位置的值 >  0 说明第一个拿的人(甲)必胜,返回true,否则返回false
        if (dp[0][piles.size() - 1] > 0) {
            return true;
        }
        else {
            return false;
        }
    }
};

 


这道题还有另外一个比较玄学的解法:

两个石子堆,我可以讲在偶数位上的石子堆分成一组,奇数位的石子堆分成一组,因为石子总和为奇数,所以这两个组石子堆必定有比较多的那一组,然后我们就会发现(我也不知道怎么发现的,别人就是这么发现了),先手的人总能够精准拿到其中的一组,所以他只要拿比较多的那一组就行。就比如,先手的人必定能拿到[0,2,4,6,8.....],或者必定能拿到[1,3,5,7...]

因此先手的人必胜,这里直接return true即可

 

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

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值