目录

题目描述

1563. 石子游戏 V

题意分析

一排石子摆在桌上,每轮 Alice 把当前这排石子从某处切成左右两段,Bob 计算两段的和,把和更大的那段扔掉,Alice 得到被保留那段的分数;如果两段和相等,则由 Alice 自己决定扔掉哪一段。重复这个过程直到只剩一颗石子,游戏结束。求 Alice 能拿到的最高总分。

尽管出现了两个人,这不是对抗博弈:Bob 的行为完全由规则决定(永远扔大的),没有任何选择权;只有在两段和相等时决定权才回到 Alice 手上。所以本质是 Alice 一个人在做决策链的最优化。

「切一刀后只保留一段,继续在这一段上切」这个形态说明子问题永远是原数组的一个连续区间,而且切割点把区间劈成两个更短的区间。区间是被完整保留下来的、不会跳跃,这直接指向以区间为状态的动态规划。

石子数上限是 500。这个数字很关键:$n^3 = 1.25 \times 10^8$ 是可以接受的,而 $n^2$ 显得过于宽松,所以出题人预期的正是三重循环的区间动规——枚举长度、左端点、切割点。

石子值都是正整数(上界 $10^6$),所以区间和严格随区间变长而增大,不存在零和负数带来的退化情况。总和最多 $5 \times 10^8$,int 装得下。

边界上,区间只剩一颗石子时游戏结束,Alice 再也拿不到分,这个状态的值是 0。

解法:区间动态规划

核心思路

每次切割后只保留连续的一侧,子问题仍是原数组的一个区间,因此使用区间 DP。

定义 dp[left][right]:当前只剩闭区间 [left,right] 时,Alice 从现在开始最多还能获得的分数。单元素区间无法再切,边界为 dp[i][i]=0

枚举切割点 split,左段是 [left,split],右段是 [split+1,right]

  • 左段和不大于右段和时,Alice 可以保留左段,收益为 leftSum + dp[left][split]
  • 右段和不大于左段和时,Alice 可以保留右段,收益为 rightSum + dp[split+1][right]

两个判断必须分别写成 <=>=。两段和不等时只有较小侧合法;相等时两个条件同时成立,恰好枚举 Alice 的两种选择。对所有切割点和合法保留侧取最大值。

状态不变量:按区间长度递增计算时,处理 dp[left][right] 前,所有严格更短区间的最优值已经确定。 每个转移只依赖左右两个更短区间,因此由长度归纳,当前状态枚举了第一刀后的全部合法选择并接上最优子问题,结果正确。

用前缀和在 $O(1)$ 时间计算任意区间和,否则三重循环内部再次求和会退化为 $O(n^4)$。

解题步骤

  1. 构造前缀和 prefix,区间 [l,r] 的和为 prefix[r+1]-prefix[l]
  2. 创建 dp[n][n],单元素状态保持 0。
  3. 区间长度从 2 增加到 n
  4. 对每个区间枚举 splitleftright-1,保证左右两段都非空。
  5. 分别按 leftSum<=rightSumleftSum>=rightSum 更新答案。
  6. 返回 dp[0][n-1]

样例 [6,2,3,4,5,5] 的最优值为 18。边界 [7] 没有合法切割,答案为 0;区间 [5,5] 两侧和相等,两种保留方向都会被检查。

代码实现

class Solution {
    public int stoneGameV(int[] stoneValue) {
        int n = stoneValue.length;
        int[] prefix = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + stoneValue[i];
        }

        int[][] dp = new int[n][n];
        for (int length = 2; length <= n; length++) {
            for (int left = 0; left + length <= n; left++) {
                int right = left + length - 1;
                for (int split = left; split < right; split++) {
                    int leftSum = prefix[split + 1] - prefix[left];
                    int rightSum = prefix[right + 1] - prefix[split + 1];

                    if (leftSum <= rightSum) {
                        dp[left][right] = Math.max(
                                dp[left][right], leftSum + dp[left][split]);
                    }
                    if (leftSum >= rightSum) {
                        dp[left][right] = Math.max(
                                dp[left][right], rightSum + dp[split + 1][right]);
                    }
                }
            }
        }
        return dp[0][n - 1];
    }
}
func stoneGameV(stoneValue []int) int {
	n := len(stoneValue)
	prefix := make([]int, n+1)
	for i, value := range stoneValue {
		prefix[i+1] = prefix[i] + value
	}

	dp := make([][]int, n)
	for i := range dp {
		dp[i] = make([]int, n)
	}

	for length := 2; length <= n; length++ {
		for left := 0; left+length <= n; left++ {
			right := left + length - 1
			for split := left; split < right; split++ {
				leftSum := prefix[split+1] - prefix[left]
				rightSum := prefix[right+1] - prefix[split+1]

				if leftSum <= rightSum {
					dp[left][right] = maxInt(
						dp[left][right], leftSum+dp[left][split])
				}
				if leftSum >= rightSum {
					dp[left][right] = maxInt(
						dp[left][right], rightSum+dp[split+1][right])
				}
			}
		}
	}
	return dp[0][n-1]
}

func maxInt(a int, b int) int {
	if a > b {
		return a
	}
	return b
}

复杂度分析

  • 时间复杂度: $O(n^3)$。共有 $O(n^2)$ 个区间,每个区间枚举 $O(n)$ 个切割点。
  • 空间复杂度: $O(n^2)$,用于状态表;前缀和占 $O(n)$。

关键点总结

  • “切一刀后只保留一侧”使状态始终是连续区间,是区间 DP 的直接信号。
  • dp[left][right] 只表示从当前区间开始的后续最优分数,不包含此前得分。
  • 按长度递增保证所有转移依赖的子区间已经计算。
  • 切割点范围必须是 [left,right-1],左右两段都不能空。
  • 相等时 Alice 可选任一侧,两条非严格不等式必须同时执行。
  • 前缀和让最内层的区间求和保持常数时间。

易错点总结

  • 相等时只保留固定一侧: 两侧后续收益可能不同,必须取两种选择的最大值。
  • 切割点取到 right 右段为空,会访问非法状态。
  • 右段从 split 开始: 会与左段重复包含切割位置;正确起点是 split+1
  • 不按区间长度计算: 可能读取尚未完成的子状态 0。
  • 单元素状态设为石子值: 单颗石子无法继续切割,后续得分应为 0。
  • 最内层重新遍历求区间和: 时间复杂度会从 $O(n^3)$ 退化到 $O(n^4)$。

相似题目

题目 难度 考察点
1690. 石子游戏 VII 中等 真正的双人对抗,状态存分差且转移取极小化极大,与本题的单方最优不同
877. 石子游戏 中等 只从两端取石子,区间状态仅有两种转移,还有先手必胜的数学结论
486. 预测赢家 中等 同为两端取数的分差动规,但要判断先手是否不败而非求最高分
312. 戳气球 困难 枚举的是「最后戳破的那个」而不是切割点,需要逆向思考区间边界
1000. 合并石头的最低成本 困难 每次合并 K 堆,状态要多一维记录当前段数,切割点还需按步长跳跃
516. 最长回文子序列 中等 转移只看两端字符是否相等,不需要枚举切割点,是区间动规的最简形态
241. 为运算表达式设计优先级 中等 同样枚举分割点合并左右结果,但返回的是全部可能值而非最优值
87. 扰乱字符串 困难 分割点两侧还要考虑是否交换,状态需要额外一维表示长度