LeetCode 486. 预测赢家
题目描述


题意分析
两名玩家轮流从数组的一端取走一个数,计入自己的得分,直到取完。双方都会让自己的最终得分尽可能高,求玩家 1 能否至少打平。每次只能取两端,所以剩余数字始终构成一个连续区间。
解法:区间 DP 计算最大分差
核心思路
[!blue]
直接记录两人的分数会引入不必要的状态。一个区间的数字总和固定,当前玩家拿得越多,对手就拿得越少,因此最大化自己的得分,等价于最大化「自己的得分减去对手的得分」。已经取走的分数不会影响剩余区间中的最优选择。
定义
dp[left][right]:只考虑剩余区间nums[left..right],双方都最优行动时,当前行动者相对另一人能取得的最大分差。这里的当前行动者随回合切换,不固定指玩家 1,分差也允许为负。当前玩家若取左端,立即得到
nums[left],随后轮到对手处理nums[left + 1..right]。子状态dp[left + 1][right]是对手相对当前玩家的优势,换回当前玩家的视角必须取反,所以这次选择的最终分差为nums[left] - dp[left + 1][right]。取右端同理,得到nums[right] - dp[left][right - 1]。两端就是全部合法选择,当前玩家选择分差较大的一个;而被减去的子状态已经包含对手的最优回应,因此这个转移同时考虑了双方的最优策略,不需要额外再枚举对手的行动。
只剩一个数时,当前玩家拿走它,对手得到 0,所以
dp[i][i] = nums[i]。从短区间算到长区间,最终dp[0][n - 1]才是玩家 1 相对玩家 2 的分差,判断它是否非负即可。
解题步骤
- 初始化
dp[i][i] = nums[i]。- 按区间长度从 2 到
n枚举,再枚举左端点并确定右端点。- 分别计算
nums[left] - dp[left + 1][right]与nums[right] - dp[left][right - 1],将较大值写入当前状态。- 返回
dp[0][n - 1] >= 0,因为平局也算先手获胜。题目保证数组非空且数字非负。只有一个数时,初始化已经给出答案;所有数字为 0 时最终分差也是 0,应返回成功。
代码实现
class Solution {
public boolean predictTheWinner(int[] nums) {
int n = nums.length;
int[][] dp = new int[n][n];
for (int idx = 0; idx < n; idx++) {
dp[idx][idx] = nums[idx];
}
for (int len = 2; len <= n; len++) {
for (int left = 0; left + len - 1 < n; left++) {
int right = left + len - 1;
// dp 表示当前玩家相对对手的最大分差,取完后角色交换。
int takeLeft = nums[left] - dp[left + 1][right];
int takeRight = nums[right] - dp[left][right - 1];
dp[left][right] = Math.max(takeLeft, takeRight);
}
}
return dp[0][n - 1] >= 0;
}
}
func predictTheWinner(nums []int) bool {
n := len(nums)
dp := make([][]int, n)
for idx := 0; idx < n; idx++ {
dp[idx] = make([]int, n)
dp[idx][idx] = nums[idx]
}
for length := 2; length <= n; length++ {
for left := 0; left+length-1 < n; left++ {
right := left + length - 1
// dp 表示当前玩家相对对手的最大分差,取完后角色交换。
takeLeft := nums[left] - dp[left+1][right]
takeRight := nums[right] - dp[left][right-1]
dp[left][right] = max(takeLeft, takeRight)
}
}
return dp[0][n-1] >= 0
}
func max(first int, second int) int {
if first > second {
return first
}
return second
}
复杂度分析
- 时间复杂度:$O(n^2)$,每个区间做常数次运算。
- 空间复杂度:$O(n^2)$。
关键点总结
[!green]
- 区间总和固定,最大化自己的得分等价于最大化相对对手的分差。
- 状态记录当前行动者的优势;轮到对手后,子问题的优势要取反。
- 依赖来自更短区间,所以按长度递增填表。
- 最外层的当前行动者是玩家 1,分差非负就满足题目的获胜条件。
易错点总结
[!yellow]
dp不是玩家 1 的固定视角,也不是当前玩家拿到的总分;混淆定义会把转移中的减号写错。- 只比较两个端点的大小,会忽略取走端点后给对手留下的局面,不能保证最终得分最大。
- 忘记初始化
dp[i][i],会让所有转移从错误基线开始。- 分差为负的子区间也必须保留,它表示轮到行动的人处于劣势;不能把负值截成 0。
- 以
> 0判断会把平局判负,最终条件必须是>= 0。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 877. 石子游戏 | 中等 | 同样从两端取石子,原题的偶数堆数和总和条件可推出特殊必胜结论,本题通常需要区间胜负差DP。 |
| 1140. 石子游戏 II | 中等 | 同样基于双方最优回应建立状态,原题每轮可取数量还由参数M决定。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!