LeetCode 1690. 石子游戏 VII
题目描述


题意分析
两人轮流移除最左或最右的一颗石子,每轮获得的是移除后剩余石子的总和。Alice 先手,希望最终的
Alice得分-Bob得分尽量大;Bob 希望这个差尽量小。要求双方都按各自目标行动时的最终分差。
解法:区间 DP
核心思路
[!blue]
把状态统一成当前玩家相对对手的最优分差。dp[l][r]表示只剩区间[l,r]时,从现在开始“当前玩家新增得分减去对手新增得分”的最大值。Bob 减小 Alice 的领先,也就是增大自己的领先,所以双方都能使用同一状态,不必额外记录是谁行动。若当前玩家移除左端,本轮得到
sum(l+1,r),接下来的[l+1,r]由对手先行动。子问题dp[l+1][r]保存的是对手相对本方的领先,因此换回本方视角,最终分差为sum(l+1,r)-dp[l+1][r]。移除右端同理,候选分差为
sum(l,r-1)-dp[l][r-1]。当前玩家只有这两种合法选择,取较大值即可;每个子问题又已包含对手的最优应对,转移就同时落实了双方的选择。过去已经获得的分数在当前两种选择下都是同一个常量,不影响后续最优决策,因此无需放进状态。单元素区间移除后已无石子,最后一轮得分为 0,故
dp[i][i]=0。每个转移只依赖长度少一的区间,按区间长度递增计算即可。
解题步骤
- 构建
prefix[i],表示前i颗石子的总和,区间[a,b]的和为prefix[b+1]-prefix[a]。- 初始化区间 DP 为 0,从长度 2 开始枚举所有
[l,r]。- 删除左端后的和为
prefix[r+1]-prefix[l+1],删除右端后的和为prefix[r]-prefix[l],分别减去对应子问题分差。- 两个候选取最大值写入
dp[l][r],最后返回dp[0][n-1]。只剩两颗石子时,对手最后一轮得 0,当前玩家应留下较大的一颗获得其分值。更长区间则必须同时考虑子问题分差,不能只按本轮得分决定删除哪一端。
代码实现
class Solution {
public int stoneGameVII(int[] stones) {
int n = stones.length;
// 带哨兵位的前缀和,sum(a, b) = prefix[b + 1] - prefix[a]。
int[] prefix = new int[n + 1];
for (int i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + stones[i];
}
// dp[l][l] = 0 由零值初始化天然满足。
int[][] dp = new int[n][n];
for (int len = 2; len <= n; len++) {
for (int l = 0; l + len - 1 < n; l++) {
int r = l + len - 1;
// 这里计算移除左端后剩余部分的和,不是被拿走的石子值。
int sumLeft = prefix[r + 1] - prefix[l + 1];
int sumRight = prefix[r] - prefix[l];
// 减去子问题结果,编码「对手也在最优行动」。
int takeLeft = sumLeft - dp[l + 1][r];
int takeRight = sumRight - dp[l][r - 1];
dp[l][r] = Math.max(takeLeft, takeRight);
}
}
return dp[0][n - 1];
}
}
func stoneGameVII(stones []int) int {
n := len(stones)
// 带哨兵位的前缀和,sum(a, b) = prefix[b+1] - prefix[a]。
prefix := make([]int, n+1)
for i := 0; i < n; i++ {
prefix[i+1] = prefix[i] + stones[i]
}
// dp[l][l] = 0 由零值初始化天然满足。
dp := make([][]int, n)
for i := 0; i < n; i++ {
dp[i] = make([]int, n)
}
for length := 2; length <= n; length++ {
for l := 0; l+length-1 < n; l++ {
r := l + length - 1
// 这里计算移除左端后剩余部分的和,不是被拿走的石子值。
sumLeft := prefix[r+1] - prefix[l+1]
sumRight := prefix[r] - prefix[l]
// 减去子问题结果,编码「对手也在最优行动」。
takeLeft := sumLeft - dp[l+1][r]
takeRight := sumRight - dp[l][r-1]
if takeLeft > takeRight {
dp[l][r] = takeLeft
} else {
dp[l][r] = takeRight
}
}
}
return dp[0][n-1]
}
复杂度分析
- 时间复杂度:$O(n²)$,每个区间比较两个候选。
- 空间复杂度:$O(n²)$,保存区间状态。
关键点总结
[!green]
- 得分来自剩余石子,不是拿走的石子。
- 子问题换了行动者,所以使用减号。
- 状态保存从当前区间开始的分差,与过去得分无关。
易错点总结
[!yellow]
- 加上对手状态:变成双方合作累计收益。
- 单元素初始化为石子值:最后拿走时实际得到零分。
- 剩余和仍包含被拿走端点:本轮得分被高估。
- 先算长区间再算依赖:使用到尚未完成的状态。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 486. 预测赢家 | 中等 | 同样按区间保存当前玩家能领先的最大分差,但本题得分是移除后剩余元素和。 |
| 1563. 石子游戏 V | 困难 | 同样用区间和快速计算游戏得分,原题按分割点留下较小一侧,本题只删除左右端点。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!