Nim 游戏 、⽯头游戏1、石头游戏2

Nim 游戏 、⽯头游戏1、石头游戏2

一:Nim 游戏

你和你的朋友,两个人一起玩 Nim 游戏:桌子上有一堆石头,每次你们轮流拿掉 1 - 3 块石头。 拿掉最后一块石头的人就是获胜者。你作为先手。

你们是聪明人,每一步都是最优解。 编写一个函数,来判断你是否可以在给定石头数量的情况下赢得游戏。

示例:

输入: 4
输出: false 
解释: 如果堆中有 4 块石头,那么你永远不会赢得比赛;
     因为无论你拿走 1 块、2 块 还是 3 块石头,最后一块石头总是会被你的朋友拿走

解题思路:

我们解决这种问题的思路⼀般都是反着思考:

1 . 如果我能赢, 那么最后轮到我取⽯⼦的时候必须要剩下 1~3 颗⽯⼦, 这样 我才能⼀把拿完。

2.如何营造这样的⼀个局⾯呢? 显然, 如果对⼿拿的时候只剩 4 颗⽯⼦, 那么⽆论他怎么拿, 总会剩下 1~3 颗⽯⼦, 我就能赢。

3.如何逼迫对⼿⾯对 4 颗⽯⼦呢? 要想办法, 让我选择的时候还有 5~7 颗⽯⼦, 这样的话我就有把握让对⽅不得不⾯对 4 颗⽯⼦。

4.如何营造 5 ~ 7 颗⽯⼦的局⾯呢? 让对⼿⾯对 8 颗⽯⼦, ⽆论他怎么拿, 都会给我剩下 5~7 颗, 我就能赢。

5.这样⼀直循环下去, 我们发现只要踩到 4 的倍数, 就落⼊了圈套, 永远逃不出 4 的倍数, ⽽且⼀定会输。 所以这道题的解法⾮常简单:

bool canWinNim(int n) {
// 如果上来就踩到 4 的倍数, 那就认输吧
// 否则, 可以把对⽅控制在 4 的倍数, 必胜
return n % 4 != 0;
}

二:⽯头游戏
亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 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. 2 <= piles.length <= 500
  2. piles.length 是偶数。
  3. 1 <= piles[i] <= 500
  4. sum(piles) 是奇数。

强调双⽅都很聪明的原因, 算法也是求最优决策过程下你是否能赢。这道题⼜涉及到两⼈的博弈, 也可以⽤动态规划算法暴⼒试, ⽐较⿇烦。 但我们只要对规则深⼊思考, 就会⼤惊失⾊: 只要你⾜够聪明, 你是必胜⽆疑的, 因为你是先⼿。


boolean stoneGame(int[] piles) {
	return true;
}

这是为什么呢, 因为题⽬有两个条件很重要: ⼀是⽯头总共有偶数堆, ⽯头的总数是奇数

这两个看似增加游戏公平性的条件, 反⽽使该游戏成为了⼀个割⾲菜游戏。

我们以 piles=[2, 1, 9, 5] 讲解, 假设这四堆⽯头从左到
右的索引分别是 1, 2, 3, 4。

  • 如果我们把这四堆⽯头按索引的奇偶分为两组, 即第 1、 3 堆和第 2、 4 堆,
  • 那么这两组⽯头的数量⼀定不同, 也就是说⼀堆多⼀堆少。 因为⽯头的总数是奇数, 不能被平分。
  • ⽽作为第⼀个拿⽯头的⼈, 你可以控制⾃⼰拿到所有偶数堆, 或者所有的奇数堆。
  • 你最开始可以选择第 1 堆或第 4 堆。 如果你想要偶数堆, 你就拿第 4 堆, 这样留给对⼿的选择只有第 1、 3 堆, 他不管怎么拿, 第 2 堆⼜会暴露出来,你就可以拿。
  • 同理, 如果你想拿奇数堆, 你就拿第 1 堆, 留给对⼿的只有第2、 4 堆, 他不管怎么拿, 第 3 堆⼜给你暴露出来了。
  • 也就是说, 你可以在第⼀步就观察好, 奇数堆的⽯头总数多, 还是偶数堆的⽯头总数多, 然后步步为营, 就⼀切尽在掌控之中了。

三、石头游戏2

亚历克斯和李继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整数颗石子 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

解题思路:

定义

  • stones:与题目中 piles 意思保持一致;
  • n:stone个数;
  • score[i, j]:当前玩家从 stones[i] 开始,且 M = j 时,该玩家所能得到的最高分数;
  • sum[i]:从stones[i]开始,剩下石头分数之和(即,stones[i] + … + stones[n - 1]);

考虑

  • 如何使得当前玩家所能获得的分数score[i, j]最高?取1个?取2个?… 取 2 * j 个?
  • 当前玩家拿1个stone时,另一个玩家接下来所能获得的最高分数为score[i + 1, max(j, 1)]
  • 当前玩家拿2个stone时,另一个玩家接下来所能获得的最高分数为score[i + 2, max(j, 2)];
  • 当前玩家拿2*j个stone时,另一个玩家接下来所能获得的最高分数为score[i + 2 * j, max(j, 2 * j)];
  • 因为sum[i]是一定的,只需要使另一个玩家接下来所能获得的最高分数最小,即可保证当前玩家所得到的分数score[i, j]最大

基于上述考虑,得到递归方程如下:


score[i,j]=sum[i]min(socre[i + 1,max(j,1)],...,score[i + 2 ∗ j,max(j,2 ∗ j)])

进一步考虑,递归终止条件:

因为所有stone的分数都为正数;当 n - i <= 2 * j 时(即,当前玩家可以直接拿下剩下的所有stone),则当前玩家所得最高分 score[i, j] 直接为剩下所有stone分数之和(即,score[i, j] = sum[i]);

完整递归方程如下:
在这里插入图片描述

回到题目

  • 一场游戏,Alex所能获得的最高分为score[0,1]
  • 对应的,Lee所获得的分数为 sum[0]−score[0,1]
  • 谁获得的分数高,谁赢

为了可以在常数时间内计算到sum[i],我们需要提前计算好后缀和数组;

当n = 8时,示例如下:

正如前面所说,当 2 * j > n - i 时,当前玩家可以拿走剩下的所有stone,因此这里 j 的上限,我们取 n 的一半向上取整;

为了计算score[0, 1],我们需要知道score[1, 1],score[2, 2],所以计算时我们采用从下到上(从左到右或者从右到左都行)的计算顺序;
在这里插入图片描述

方法一:DP 函数

class Solution {
public:

    int n = 0;
    //表示从i开始取M个获取的最大价值
    int* vec;

    //备忘录
    int mem[105][105];

    //dp函数
    int dp(int s, int M) 
    {
        //如果在备忘录中直接在备忘录中返回,避免重复计算
        if(mem[s][M] != 0) 
            return mem[s][M];

        //如果s >= n代表下标已经越界,相当于piles越界
        if(s >= n) 
            return 0;

        //如果发现 s + 2 * M >= n 说明玩家可以一次取完,直接返回
        if(s + 2 * M >= n)
        {
            return vec[s];
        }
        
        int ans = 0;

        for(int i = 1; i <= 2 * M; i++) 
        {
            //当前的最大价值为,当前剩余价值 - 下一个状态的最大价值。
            ans = max(ans, vec[s] - dp(s + i, max(i, M)));            
        }

        //记忆化搜索
        mem[s][M] = ans;
        return ans;
    }
    
    int stoneGameII(vector<int>& piles) {
        n = piles.size();

        //初始化相关操作
        memset(mem, 0, sizeof(mem));
        vec = new int[n];

        //从 后往前计算每个 i 位置前 的所有石头之和
        int sum = 0;
        for(int i = n - 1; i >= 0; i--) {
            sum += piles[i];
            vec[i] = sum;
        }
  
        //递归调用dp函数求结果,0:代表玩家从piles的0下标处开始可以获得的最大石头数量 ;1:代表m的初始值为1
        return dp(0,1);
    }
};

方法二:DP table

class Solution {
public:
    int stoneGameII(vector<int>& piles) {
        int len = piles.size();
        
        //用来从前往后计算每个 i 位置的所有石头之和
        int sum = 0; 
        // dp[i][j]表示当前是第i波,m = j,玩家所能获得的最大石头的数量;
        vector<vector<int>>dp(len + 1, vector<int>(len + 1, 0));

        //注意要反向遍历
        for(int i = len - 1; i >= 0; i--)
        {
            sum += piles[i];// 表示当前所剩下的所有棋子的和
            for(int M = 1; M <= len; M++)
            {
                //代表玩家可以一次取完剩下的所有石头,直接放到dp中,继续循环测试
                if(i + 2 * M >= len)
                {
                    //直接把剩下的所有石头保存在dp数组中
                    dp[i][M] = sum;
                    continue;
                }

                //用循环来枚举所有的情况,取其中玩家可以获得最大石头数量的一个结果保存
                //x代表每次取的堆数
                //i + x <= len 是为了防止越界
                //x <= 2 * M 代表每次可以取的石头的堆数范围
                for(int x = 1; i + x <= len && x <= 2 * M; x++)
                {
                    //每次选取其中结果最大 的来保存
                    dp[i][M] = max(dp[i][M], sum - dp[i + x][max(M, x)]);
                }
            }
        }
        return dp[0][1];
    }
};


发布了126 篇原创文章 · 获赞 57 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/wolfGuiDao/article/details/104594074
今日推荐