「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」。
题目
给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。
玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0] 或 nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。
如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。
示例 1:
输入:nums = [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 。
示例 1:
输入:nums = [1,5,233,7]
输出:true
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 true,表示玩家 1 可以成为赢家。
思路
正常动态规划的题目,做的多了,也慢慢有了感觉了。
首先明确一下,因为每次拿数字都只能拿数组的开始或者结束,所以任何一次拿完之后,数组依然是连续的,不会被截断。这点非常重要,是后面推导的基础。
定义二维数组dp[][],dp[i][j]代表如果剩下从i到j的字符串时,当前先手方最优解的净胜值。最终我们要求的,可以转化成是否满足 dp[0][len-1] >= 0,如果满足就是先手赢家(PS:有等于因为得分相等也是先手赢家)。
在dp状态下,显然当前先手方有2种选择
- 如果选择拿num[i],那么dp[i][j] = num[i] - dp[i+1][j],因为拿掉num[i]后,当前先手方变成dp[i+1][j]状态的后手方,所以要减去dp[i+1][j]
- 如果选择拿num[j],那么同理可得,dp[i][j] = num[j] - dp[i][j-1] 综合起来,状态转移方程为 dp[i][j] = max(num[i] - dp[i+1][j], num[j] - dp[i][j-1]) 对于i==j的特殊情况,当前先手方拿掉这个数字后,不剩下数字了,所以dp[i][i] = num[i]。 这样,我们就可以用dp来求解了,注意到dp[i][j]只跟dp[i+1][j]、dp[i][j-1]相关,一个是当前这一行的值,一个是后一行的值,所以我们可以把dp降为一维数组,这是一种dp的常规优化,这里不展开。
彩蛋
做完之后,参看其他人的解法,发现有一个比较有意思的结论:对于偶数个数字的数组,玩家1一定获胜。因为如果玩家1选择拿法A,玩家2选择拿法B,玩家1输了。则玩家1换一种拿法选择拿法B,因为玩家1是先手,所以玩家1一定可以获胜。
所以,可以进一步优化成,偶数直接判断先手赢,奇数再dp,可能可以带来50%的提升。
Java版本代码
class Solution {
public boolean PredictTheWinner(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
for (int i = 0; i < len; i++) {
dp[i] = nums[i];
}
for (int i = len-2; i>=0; i--) {
for (int j = i+1; j < len; j++) {
dp[j] = Integer.max(nums[i] - dp[j], nums[j] - dp[j - 1]);
}
}
return dp[len-1] >= 0;
}
}