486. 预测赢家
思路:
这一题我也是没想出来,题目都看错了,我以为是随便取数,没想到是从两端取。。。
首先这个题目可以观察出一个规律,就是当数组长度为偶数时,先手必胜。假设数组为[1, 2, 3, 4],则可以将数组分为奇数列[1, 3]和偶数列[2, 4],其和分别为4,6。此时如果先手选择了右边的4,也就是偶数列中的数,那么数组变为[1, 2, 3],此时数组两边的数都是原数组中奇数列中的数,所以后手被迫只能选择奇数列中的数,比如3,然后先手再选择偶数列中的数字,也就是2,那么后手也是只能选择奇数列中的数,也就是1,最后先手获得6分,后手获得4分,先手胜。(简单来说,就是先手直接决定了后手只能选奇数还是偶数,先手才是主导者)
实际上我们也并不需要使用到这个规律(可用可不用),现在开始最简单的解法,暴力递归:
递归函数返回先手的最大取数之和,然后与后手取数之和(总和减去先手最大取数之和)对比即可:
public boolean PredictTheWinner(int[] nums) {
int sum = 0;
for(int n : nums)
sum += n;
int first = f(nums, 0, nums.length-1);
return first >= (sum - first);
}
对于递归函数首先定义出口,当数组长只剩1时那么只能选择这个数,如果数组长只剩2时那么选择最大的即可,因为无论是先手还是后手都会选择对自己最有利的!
if(i == j)
return nums[i];
if(i+1 == j)
return Math.max(nums[i], nums[j]);
最后递归的返回先手的累加和,这里需要注意,我们需要分取【i,j】的i还是j两种情况,例如:当我们先手选i时后手会在【i+1,j】中选择最大的端点,那么后手留给先手的一定是【i+2,j】或者【i+1,j-1】中的最小一个,取最小就行。
return Math.max(
nums[i] + Math.min(f(nums, i+1, j-1), f(nums, i+2, j)),
nums[j] + Math.min(f(nums, i+1, j-1), f(nums, i, j-2)));
完整代码如下:
public boolean PredictTheWinner(int[] nums) {
int sum = 0;
for(int n : nums)
sum += n;
int first = f(nums, 0, nums.length-1);
return first >= (sum - first);
}
private int f(int[] nums, int i, int j) {
if(i == j)
return nums[i];
if(i+1 == j)
return Math.max(nums[i], nums[j]);
return Math.max(
nums[i] + Math.min(f(nums, i+1, j-1), f(nums, i+2, j)),
nums[j] + Math.min(f(nums, i+1, j-1), f(nums, i, j-2)));
}
同样的,这个递归可以改成动态规划,其中递归的出口就是动态数组的初始化:
int len = nums.length;
int[][] dp = new int[len][len];
for(int i = 0; i < len; i++)
dp[i][i] = nums[i];
for(int j = 1; j < len; j++)
dp[j-1][j] = Math.max(dp[j-1][j-1], dp[j][j]);
我们已经初始化两条动态数组的对角线,因此我们就从对角线开始更新(只用更新数组的右上半部分):
for(int i = 2; i < len; i++)
for(int row = 0; i + row < len; row++)
dp[row][row+i] = Math.max(nums[row] + Math.min(dp[row+1][i+row-1], dp[row+2][i+row]),
nums[i+row] + Math.min(dp[row][i+row-2], dp[row+1][i+row-1]));
内循环也可以倒过来:
for(int i = 2; i < len; i++)
for(int row = len-i-1; row>=0; row--)
dp[row][row+i] = Math.max(nums[row] + Math.min(dp[row+1][i+row-1], dp[row+2][i+row]),
nums[i+row] + Math.min(dp[row][i+row-2], dp[row+1][i+row-1]));
完整代码如下:
public boolean PredictTheWinner(int[] nums) {
int sum = 0;
for(int n : nums)
sum += n;
int len = nums.length;
int[][] dp = new int[len][len];
for(int i = 0; i < len; i++)
dp[i][i] = nums[i];
for(int j = 1; j < len; j++)
dp[j-1][j] = Math.max(dp[j-1][j-1], dp[j][j]);
// 按照对角线来递推
for(int i = 2; i < len; i++)
for(int row = 0; i + row < len; row++)
dp[row][row+i] = Math.max(nums[row] + Math.min(dp[row+1][i+row-1], dp[row+2][i+row]),
nums[i+row] + Math.min(dp[row][i+row-2], dp[row+1][i+row-1]));
return dp[0][len-1] >= (sum - dp[0][len-1]);
}
像过程分区间的题目,首先想到的就是矩阵三角式动态规划,按副对角线方向计算。
以上两种方式都是站在先手的角度进行递归和动态规划,我在评论区看到一种更为简洁的方法:
对于区间[i,j],先手选择一个数减去剩下区间对手获得的最大领先(对手在这个区间先手赢多少),大于等于0就是赢。
1.选择i,那结果就是rsp1 = nums[i] - dp[i+1][j](dp[i+1][j]为对手最优策略)
2.选择j,那结果就是rsp2 = nums[j] - dp[i][j-1](dp[i][j-1]为对手最优策略)
由于规定每个人都是贪婪的,dp[i][j] = max(rsp1,rsp2) 计算方向就是沿着副对角线的方向,一层层计算主对角线,直到[0,len-1]
public boolean PredictTheWinner(int[] nums) {
int len = nums.length;
int[][] dp = new int[len][len];
for(int k=0;k<len;k++){
for(int i=0,j=i+k;j<len;i++,j++){
if(k==0)
dp[i][j] = nums[i];
else{
dp[i][j] = Math.max(nums[i]-dp[i+1][j],nums[j]-dp[i][j-1]);
}
}
}
return dp[0][len-1]>=0;
}