486. 预测赢家
给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。
Trick
当数组长度为偶数时,玩家1必赢。
因为偶数长度的数组,两个玩家能够取的数的个数是一样的,则可以遍历完每一种取法,玩家1直接使用可以获得最高分的取法即可。
方法一:递归
思想:由于每次只能从数组的任意一端拿取数字,因此可以保证数组中剩下的部分一定是连续的,所以可以递归进行不断分解原问题,直至最小子问题(最后剩余的数组只有一个元素)。
递归终止条件:
1. 若当前数组只有一个元素,此时直接返回此元素的所属者,即选手1还是选手2(先手还是后手)
2. 若当前数组含有多于一个元素,则比较:拿第一个,还是拿最后一个得分更高。若剩余数组长度大于1,则重复执行2,继续递归。
- python
class Solution:
### 官方解
def PredictTheWinner(self, nums: List[int]) -> bool:
def total(start: int, end: int, turn: int) -> int:
if start == end:
return nums[start] * turn
scoreStart = nums[start] * turn + total(start + 1, end, -turn)
scoreEnd = nums[end] * turn + total(start, end - 1, -turn)
return max(scoreStart * turn, scoreEnd * turn) * turn
return total(0, len(nums) - 1, 1) >= 0
### 个人解
def PredictTheWinner(self, nums):
def leftMax(L, R):
if L == R:
return nums[L]
scoreL = nums[L] - leftMax(L+1, R)
scoreR = nums[R] - leftMax(L, R-1)
return max(scoreL, scoreR)
return leftMax(0, len(nums)-1) >= 0
- C++
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
return total(nums, 0, nums.size() - 1, 1) >= 0;
}
int total(vector<int>& nums, int start, int end, int turn) {
if (start == end) {
return nums[start] * turn;
}
int scoreStart = nums[start] * turn + total(nums, start + 1, end, -turn);
int scoreEnd = nums[end] * turn + total(nums, start, end - 1, -turn);
return max(scoreStart * turn, scoreEnd * turn) * turn;
}
};
复杂度分析
时间复杂度:O(2n),其中 n 是数组的长度。
空间复杂度:O(n),其中 n 是数组的长度。空间复杂度取决于递归使用的栈空间。
方法二:动态规划(Dynamic Programming)
- python
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
length = len(nums)
dp = [[0] * length for _ in range(length)]
for i, num in enumerate(nums):
dp[i][i] = num
for i in range(length - 2, -1, -1): # 行数从倒数第二行开始,递减,直至最开始一行
for j in range(i + 1, length): # 列数从当前行数加一开始,递增,直至最后一列
# nums[i] - dp[i + 1][j]表示拿左端的值与剩下的最大值的差
# nums[j] - dp[i][j - 1]表示拿右端的值与剩下的最大值的差
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
# 由于选手1为全局先手,因此需要查看 j - i == length - 1 的元素值是否大于等于0,即全局查看(跨度为整个数组的长度)
return dp[0][length - 1] >= 0
空间优化后,使用一维数组:
class Solution:
def PredictTheWinner(self, nums: List[int]) -> bool:
length = len(nums)
dp = [0] * length # 空间优化,使用一维数组
for i, num in enumerate(nums):
dp[i] = num # 初始化一维数组为原数组,在后面进行动态规划时,数组dp中每位将动态表示为此时取此位能够得到的最大值/最优解
for i in range(length - 2, -1, -1): # 考虑最后的最小的一个子数组,从后往前还成原数组,每次考虑获胜最多/最优的情况(贪心策略)
for j in range(i + 1, length): # 每次都要与i之后的所有j来计算一次,以获得最优解
# nums[i] - dp[i + 1][j]表示拿左端的值与剩下的最大值的差
# nums[j] - dp[i][j - 1]表示拿右端的值与剩下的最大值的差
# dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1])
dp[j] = max(nums[i] - dp[j], nums[j] - dp[j - 1])
return dp[length - 1] >= 0 # dp[0][length - 1] >= 0
- C++
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
int length = nums.size();
auto dp = vector<vector<int>> (length, vector<int>(length));
for (int i = 0; i < length; i++) {
dp[i][i] = nums[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[i][j] = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
}
}
return dp[0][length - 1] >= 0;
}
};
空间优化后,使用一维数组:
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
int length = nums.size();
auto dp = vector<int>(length);
for (int i = 0; i < length; i++) {
dp[i] = nums[i];
}
for (int i = length - 2; i >= 0; i--) {
for (int j = i + 1; j < length; j++) {
dp[j] = max(nums[i] - dp[j], nums[j] - dp[j - 1]);
}
}
return dp[length - 1] >= 0;
}
};
复杂度分析
877. 石子游戏
class Solution:
def stoneGame(self, piles: List[int]) -> bool:
length = len(piles)
dp = [[0] * length for _ in range(length)]
for i, pile in enumerate(piles):
dp[i][i] = pile
for i in range(length - 2, -1, -1):
for j in range(i + 1, length):
dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])
return dp[0][length - 1] > 0
- 解法2(数学方法)