气球游戏(DP,分治)
气球游戏
时间限制 10000 ms
内存限制 64 MB
题目描述
刚刚今天去游乐场玩,发现了一个新的游戏项目,游戏是这样的,场上一共有 n 个气球,它们的编号是0到n-1,然后每个气球上还有一个数字,我们使用数组nums来保存这些数字。
现在游戏要求刚刚戳破所有的气球。每当刚刚戳破一个气球i时,刚刚可以获得nums[left] * nums[i] * nums[right]个积分。这里的left和right指的是和i相邻的两个气球的序号。(注意每当刚刚戳破了气球i后,气球left和气球right就变成了相邻的气球。)
求所能获得积分的最大值。
输入数据
输入中有若干组测试样例,第一行为一个正整数T(T≤1000),表示测试样例组数。每组测试样例包含2部分: 第一部分有一行,包含1个正整数n(0≤n≤500),第二部分为一行,有n个数,第i个数表示num[i],(0≤num[i]≤100)。
输出数据
对每组测试数据,单独输出一行答案,表示最大的积分值
样例输入
1
4
3 1 5 8
样例输出
167
核心挑战 (为什么不能用贪心):
当我们戳破一个气球时,它会永久地改变剩余气球的相邻关系。
-
例如:[A, B, C]。如果先戳 B,得分 A · B · C,剩下 [A, C]。
-
如果先戳 A,得分 1⋅A⋅B(假设左边是虚拟气球 1),剩下 [B,C]。
每一步的“最佳”选择(获得最高局部积分)并不能保证得到全局最优解。传统的动态规划方法(比如 DP[i][j] 表示戳破 i 到 j 气球的最大积分)很难定义,因为子问题 [i,k] 和 [k+1,j] 的边界在合并时是不确定的(取决于它们之外的气球是否被戳破)。
动态规划(DP)的巧妙转化:逆向思维
为了消除相邻关系变化带来的不确定性,我们采用逆向思考,即固定区间内最后一个被戳破的气球。
1. 预处理:添加虚拟气球
首先,为了简化边界处理,我们在原始数组 nums 的两端各添加一个值为 1 的虚拟气球。这些虚拟气球是不能被戳破的,它们的作用是充当边界。
-
原始数组 nums 长度为 n。
-
新数组 A = [1, nums_0, nums_1, ..., nums_n-1, 1],长度为 N = n+2。
-
原 nums 对应 A 的索引范围是 1 到 n。
2. 定义 DP 状态
我们定义 DP[i][j] 为:
DP[i][j] 表示戳破开区间 (A[i], ..., A[j]) 内所有气球所能获得的最大积分。
-
这里的 A[i] 和 A[j] 都是未被戳破的气球,它们充当了区间 (i, j) 的左右边界。
-
我们的最终目标是 DP[0][n+1],即戳破 A[0] 和 A[n+1] 之间的所有气球。
3. 状态转移方程(核心)
要计算 DP[i][j],即最大化戳破 A[i+1], ..., A[j-1]$ 的积分,我们选择一个气球 A[k] (i < k < j),让它成为这个区间内最后一个被戳破的气球。
如果 A[k] 是最后一个被戳破的:
-
在戳破 A[k] 的瞬间,它左右两边的相邻气球一定是 A[i] 和 A[j](因为区间内其他气球都已被戳破)。
-
戳破 A[k] 获得的积分是:A[i] × A[k] × A[j]。
-
在 $A[k]$ 被戳破之前,整个问题被分解成两个相互独立的子问题:
-
左边子问题: 戳破开区间 (A[i], ..., A[k]) 内的气球。最大积分为 DP[i][k]。
-
右边子问题: 戳破开区间 (A[k], ...., A[j]) 内的气球。最大积分为 DP[k][j]。
-
因此,如果选择 A[k] 为最后一个,总积分就是三个部分之和:
Score(k) = DP[i][k] + DP[k][j] + A[i] × A[k] × A[j]
由于 k 可以是 i+1 到 j-1 之间的任何一个气球,我们必须尝试所有可能的 k,取最大值:
DP[i][j] = max_{i < k < j} { DP[i][k] + DP[k][j] + A[i] × A[k] × A[j] }
这个巧妙的转化是解决此问题的关键。
4. DP 的计算顺序(自底向上)
由于 DP[i][j] 的计算依赖于更小的区间 DP[i][k]和 DP[k][j],我们需要按区间长度 ell 从小到大进行计算。
-
区间长度 ell: 从 2 开始,到 N=n+2 结束。 (ell = j - i)
-
左边界 i: 从 0 开始,到 N - ell 结束。
-
右边界 j: $j = i + ell。
-
枚举 k: 从 i+1 到 j-1。
代码
#include <iostream>
#include <vector>
#include <algorithm>using namespace std;// 解决气球游戏问题的函数
long long maxCoins(const vector<int>& nums) {int n = nums.size();if (n == 0) {return 0;}// Step 1: 预处理 - 构建新的气球数组 A// 在原始数组两端添加值为 1 的虚拟气球,简化边界处理// 数组 A 的长度为 N = n + 2int N = n + 2;vector<int> A(N);A[0] = 1;A[N - 1] = 1;for (int i = 0; i < n; ++i) {A[i + 1] = nums[i];}// Step 2: 初始化 DP 表// DP[i][j] 表示戳破开区间 (A[i], ..., A[j]) 内所有气球的最大积分// i 和 j 是未被戳破的边界气球的索引// 使用 long long 以防积分溢出 (nums[i] <= 100, 100*100*100 = 1,000,000,虽然不会溢出 int,但为了安全和良好的习惯,使用 long long)vector<vector<long long>> dp(N, vector<long long>(N, 0));// Step 3: 填充 DP 表(按区间长度从小到大)// ell 是开区间 (i, j) 中包含的气球数量,即 j - i 的长度// 从最短区间长度 l=2 (包含 1 个气球) 开始for (int ell = 2; ell < N; ++ell) { // 区间长度 ell: 从 2 到 N-1 (最长区间 [0, N-1])for (int i = 0; i < N - ell; ++i) { // 区间左边界 iint j = i + ell; // 区间右边界 j// 枚举 k,即 (i, j) 区间内最后一个被戳破的气球 A[k]// k 的范围是 i+1 到 j-1for (int k = i + 1; k < j; ++k) {// 状态转移方程:// DP[i][k]: 戳破 (i, k) 区间内气球的最大积分// DP[k][j]: 戳破 (k, j) 区间内气球的最大积分// A[i] * A[k] * A[j]: 气球 A[k] 最后被戳破时获得的积分 (此时 A[i] 和 A[j] 是其相邻气球)long long current_score = dp[i][k] + dp[k][j] + (long long)A[i] * A[k] * A[j];// 更新最大积分dp[i][j] = max(dp[i][j], current_score);}}}// Step 4: 返回结果// 最终答案是 DP[0][N-1],即戳破 A[0] 和 A[N-1] 之间的所有气球的最大积分return dp[0][N - 1];
}// 主函数处理输入和输出
int main() {// 优化输入输出速度ios_base::sync_with_stdio(false);cin.tie(NULL);int T;if (!(cin >> T)) return 0; // 读取测试样例组数while (T--) {int n;if (!(cin >> n)) break; // 读取气球数量vector<int> nums(n);for (int i = 0; i < n; ++i) {if (!(cin >> nums[i])) break; // 读取气球上的数字}long long result = maxCoins(nums);cout << result << "\n";}return 0;
}