本文正在参加「Java主题月 - Java开发实战」,详情查看:juejin.cn/post/696719…
这是我参与更文挑战的第5天,活动详情查看: 更文挑战
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 可以成为赢家。
提示:
1 <= 给定的数组长度 <= 20. 数组里所有分数都为非负数且不会大于 10000000 。 如果最终两个玩家的分数相等,那么玩家 1 仍为赢家。
方法签名
public boolean PredictTheWinner(int[] nums) {
}
First Try 2 Pass
根据之前做过的石子游戏模板套进去:
public static boolean PredictTheWinner(int[] nums) {
return predict(nums,0,nums.length-1,0,1) >= 0;
}
public static int predict(int[] nums,int left,int right,int total,int turn){
int dix = turn%2==0?-1:1;
if(left>right){
return total;
}
int l = predict(nums,left+1,right,total + dix*nums[left],turn+1);
int r = predict(nums,left,right-1,total + dix*nums[right],turn+1);
return Math.max(l,r);
}
直接就挂了,【1,5,2】案例都过不去。
分析
通过debug看该case进行的过程,发现:
-
在根据上一步结果到下一步结果的处理时,是根据 当前操作者可以得出的最佳结果计算的,那么就不符合题目中的:
- 每个玩家的玩法都会使他的分数最大化
因此在拿到上一步的结果时,会根据当前的结果选择最大的,那么上一步操作者的选择对于下一步操作而言,反而是取两个结果中最小的了。
因此我们更换思路:
因为结果是从下面往上计算的,实际上:
-
上一步的结果对接收到结果的玩家而言,一定是要减去的。这意味着:
-
我们不需要total这个值来记录总的结果值。
-
我们不需要turn来记录轮次,拿到上一批的结果直接减去即可。
那么我们得出新的递归公式:
F(l,r) = max(num[l]-F(l+1,r),num[r]-F[l,r-1])
使用递归实现,代码如下:
public static boolean PredictTheWinner(int[] nums) { return predict(nums,0,nums.length-1) >= 0; } public static int predict(int[] nums,int left,int right){ if(left>=right) return nums[left]; int l = predict(nums,left+1,right); int r = predict(nums, left, right-1); return Math.max(nums[left]-l,nums[right]-r); }结果:
执行用时:65 ms, 在所有 Java 提交中击败了19.81%的用户
内存消耗:35.8 MB, 在所有 Java 提交中击败了44.14%的用户
-
优化递归
备忘录
思路对了,接下来我们要通过一些其他的手段,来简化递归。
实际上在[l,r]区间,F(l,r)结果是一定的,那么我们直接用二维数组来记录结果,同时用循环替代递归,减少递归造成的栈帧过多。
记dp[i] [j]为i,j区间上的结果F(i,j),那么得出实现的框架为:
public static boolean PredictTheWinner(int[] nums) {
int[][] dp = new int[nums.length][nums.length-1];
for(?){
//todo
}
return dp[0][nums.length-1]>=0;
}
初始化DP数组
根据递归的返回条件
if(left>=right) return nums[left];
我们在
i = j
的位置,在dp数组上填上nums[i]:
for(int i=0;i<nums.length;i++){
dp[i][i] = nums[i];
}
递归逻辑转化为循环逻辑
根据我们对于dp数组的定义:
dp[i] [j] = F(i,j)
假设我们对dp数组的定义是对的,那么我们的递归逻辑
F(l,r) = max(num[ l ]-F( l+1 , r ),num[ r ]-F[ l, r-1 ])
在引入dp数组后,就变成了:
dp[i] [j] = Math.max(nums[i]-dp[i+1] [j],nums[j]-dp[i] [j-1]);
随后根据我们对dp数组初始化的位置,我们容易得到:
dp[length] [length]结果可知
而且我们的**隐含逻辑(本题中引入变量的共识)**是:
r >= l
而且我们通过初始化dp,提前将 r = l的情况固化到dp数组中了,那么循环中:
r >= l+1
我们如果从
l == 0
往后来填dp的格子,会面临dp[i+1] [j] 和dp[i] [j-1]不知道是多少的情况,因此我们从已知入手,从dp[ length] [length ]来。
已知:
dp[length] [length],dp[length-1] [length-1]
我们令公式中的
i+1 = length,j-1 = length-1
那么就可以得到初始起始位置:
i = length -1, j = length
那么dp循环填入的代码就如下:
for(int i=nums.length-2;i>=0;i--){
for(int j=i+1;j<nums.length;j++){
dp[i][j] = Math.max(nums[i]-dp[i+1][j],nums[j]-dp[i][j-1]);
}
}
组合起来就变成最终答案
public static boolean PredictTheWinner2(int[] nums) {
int[][] dp = new int[nums.length][nums.length];
for(int i=0;i<nums.length;i++){
dp[i][i] = nums[i];
}
for(int i=nums.length-2;i>=0;i--){
for(int j=i+1;j<nums.length;j++){
dp[i][j] = Math.max(nums[i]-dp[i+1][j],nums[j]-dp[i][j-1]);
}
}
return dp[0][nums.length-1]>=0;
}
运行结果:
执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
内存消耗:35.9 MB, 在所有 Java 提交中击败了30.20%的用户
滚动数组
备忘录的方法大大减少了递归重复计算的次数,但是对于dp数组,由于我们的定义:
i <= j
因此在i>j的位置上,dp数组对应的值是没有意义的,例如上面的case【1,5,2】,最终的dp结果为:
[1,4,-2]
[0,5,3]
[0,0,2]
这些空间就被浪费了。在优化空间上,我们希望去除这种空间上的冗余。
推导
根据我们的公式,用case[1,5,2]来举例,事实上我们只在乎:
dp[0] [1] 和 dp[1] [2]
dp[0] [1] - dp[0] [0] 和dp[1] [1]
dp[1] [2] - dp[1] [1] 和dp[2] [2]
用例子中的另一个case【1,5,233,7】,最后输出dp数组时,我们可以发现:
事实上我们只在乎上一组数据和当前组的数据,并且:
上一组数据和当前组数据在第二个位置上永远是上一组数据的要小于当前数据的
那么我们其实就不用一个二维数组来存数据了,我们用一个一维数组,记录上一步计算出的的数据,同时后面的数据使用当前组的覆盖掉就好了。
那么我们得出以下结果:
public static boolean PredictTheWinner3(int[] nums) {
int[] dp = new int[nums.length];
for(int i=0;i<nums.length;i++){
dp[i] = nums[i];
}
for(int i=nums.length-2;i>=0;i--){
for(int j=i+1;j<nums.length;j++){
dp[j] = Math.max(nums[i]-dp[j],nums[j]-dp[j-1]);
}
}
return dp[nums.length-1]>=0;
}