LeetCode 877. 石子游戏 / 486. 预测赢家

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode 877. 石子游戏 / 486. 预测赢家相关的知识,希望对你有一定的参考价值。

877. 石子游戏

2021.6.16每日一题

题目描述

亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 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 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/stone-game
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

我记得之前有一天的每日一题就是这个,直接返回true就行了
因为石子堆的个数是偶数,那么按照下标把石子堆分成偶数堆和奇数堆
第一轮第一个人选取,可以选0或n-1,也就是一个偶数一个奇数,如果选偶数,剩下的人只能在两个奇数中选一个;如果选奇数,剩下的人只能在偶数中选一个
那么第一轮第二个人选的时候,不管第一次剩下的是什么,他在选择以后,肯定左右两边的奇偶性又不相同,
所以第一个人再选择的时候,还是可以选与第一轮选的奇偶性相同的堆,而剩下的堆奇偶性也和第一轮相同,也就是说第二个人被迫也会选石子堆的奇偶性与第一轮相同
后面同理,也就是说,第一个人可以通过他选择奇数堆或者偶数堆来迫使第二个人选择另一种石子堆;而总的石子个数是奇数,也就是说奇数堆和偶数堆肯定有一个比较大,第一个人选择较大的堆就可以一定保证胜利

class Solution {
    //看了题解,我笑了,这是个什么鬼呢
    //就是因为石子堆数是偶数,那么就可以按下标是偶数或者奇数分为堆数相同的两组0 2 4 6.. 和 1 3 5 7 ...
    //作为先手,那么可以选择偶数0或者奇数组n-1选一个,不管选哪个组,剩下的另一个人必须在奇数组或者偶数组一组中选择(对应前面)
    //同理,第三轮,在前两轮选择完毕后,可以发现,先手的人依旧可以选与第一轮相同的组,那么强迫后手的人也必须选第二轮选的组
    //那么,这样就可以得出一个结果,就是先手者可以决定选奇数组或者偶数组
    //而石子的总数是奇数,那么奇数组和偶数组肯定有一个的和是大的,因此先手的人必胜
    public boolean stoneGame(int[] piles) {
        return true;
    }
}

常规方法:动态规划
dp[i][j]表示当前石子剩下下标为 i 到 j 时,两个人石子数量最大差值
而状态转移方程为:
dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
感觉还是不太好理解的,因为还有什么先后手,差值还有正负
可以这样想,每个人的想法其实都是想让这个差值最大,而动态规划过程中,其实每一次决策都是将他自己看成先手,所以对于dp[i][j],作为先手方,可以选第i堆石子,剩下dp[i + 1][j];或者第j堆石子,剩下dp[i][j - 1]
而如果剩下dp[i + 1][j]时,是以当前方为先手所计算的结果,那么dp[i][j]就是计算当前选的石子数与剩余结果的差值,也就是piles[i] - dp[i + 1][j],另一种情况也同理

class Solution {
    //再写个动规的
    //怎么定义呢,dp[i][j]表示当前石子剩下下标为i到j时,两个人石子数量最大差值
    public boolean stoneGame(int[] piles) {
        int l = piles.length;
        int[][] dp = new int[l][l];
        //初始化
        for(int i = 0; i < l; i++){
            dp[i][i] = piles[i];
        }

        //状态转移,当剩下石子为[i]到[j]时,可以选择第i堆和第j堆,
        //这里转移的时候,直接做差就行了
        //dp[i][j] = max(piles[i] - dp[i + 1][j], piles[i] - dp[i][j - 1])
        //因为i和i+1有关,j和j-1有关,所以i遍历顺序为从大到小,j为从小到大
        for(int i = l - 2; i >= 0; i--){
            for(int j = i + 1; j < l; j++){
                dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
            }
        }
        return dp[0][l - 1] > 0;
    }
}

486. 预测赢家

题目描述

给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。

给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。

 
示例 1:

输入:[1, 5, 2]
输出:False
解释:一开始,玩家1可以从1和2中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 False 。
示例 2:

输入:[1, 5, 233, 7]
输出:True
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
     最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 True,表示玩家 1 可以成为赢家。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/predict-the-winner
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

动规的思路和上面那个题基本一模一样,之前写的代码

class Solution {
    public boolean PredictTheWinner(int[] nums) {
        //定义二维数组dp[i][j],其行数和列数都等于数组的长度,dp[i][j]表示当数组剩下的部分为下标 i 到下标 j 时,
        //当前玩家与另一个玩家的分数之差的最大值,注意当前玩家不一定是先手。
        //然后dp的转移就是取左边和取右边的最大者
        //动态规划不好理解,说递归是自上而下,这道题就是从整个数组到单个数
        //动规是自底而上,那么应该就是从单个数到整个数组规划
        int l = nums.length;
        int[][] dp = new int[l][l];
        //初始化,当i和j相等的时候,只能取这个值,那么就是nums[i]
        for(int i = 0; i < l; i++){
            dp[i][i] = nums[i];
        }
        //转移,应该从下到上,从左到右转移
        for(int i = l - 2; i >= 0; i--){
            for(int j = i + 1; j < l; j++){
                dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
            }
        }
        return dp[0][l - 1] >= 0;
    }
}

递归写法

class Solution {
    public boolean PredictTheWinner(int[] nums) {
        return play(nums, 0, nums.length - 1) >= 0;
    }
    //看了这个递归一下子感觉通透很多,就是不要轮次的表示,每次计算的都是从lo到hi这个数组内,
    //取左边或者取右边所得到的分数的最大值
    //而这个最大值对于上一层的人来说,是负的,因此要减去
    //这里也间接的表明了刚刚动规方法中为什么没有轮次表示
    //动规其实也是这个道理,dp[i][j]只表示i到j这个区间内,所得分数的最大值,是从取左边或者取右边转移过来的,
    //所以是用减号
    private int play(int[] nums, int lo, int hi) {
        if (lo > hi) return 0;
        int planA = nums[lo] - play(nums, lo + 1, hi);
        int planB = nums[hi] - play(nums, lo, hi - 1);
        return Math.max(planA, planB);
    }
}

记忆化递归

class Solution {
    public boolean PredictTheWinner(int[] nums) {
        int l = nums.length;
        int[][] memo = new int[l][l];

        for (int i = 0; i < l; i++) {
            Arrays.fill(memo[i], Integer.MIN_VALUE);
        }
        return dfs(nums, 0, l - 1, memo) >= 0;
    }

    private int dfs(int[] nums, int i, int j, int[][] memo) {
        if (i > j) {
            return 0;
        }

        if (memo[i][j] != Integer.MIN_VALUE) {
            return memo[i][j];
        }
        int chooseLeft = nums[i] - dfs(nums, i + 1, j, memo);
        int chooseRight = nums[j] - dfs(nums, i, j - 1, memo);
        memo[i][j] = Math.max(chooseLeft, chooseRight);
        return memo[i][j];
    }
}

以上是关于LeetCode 877. 石子游戏 / 486. 预测赢家的主要内容,如果未能解决你的问题,请参考以下文章

Leetcode(877)-石子游戏

leetcode 877. 石子游戏

LeetCode 877石子游戏[动态规划 数学] HERODING的LeetCode之路

877. 石子游戏

877. 石子游戏

877. 石子游戏