题目链接
题目描述
在一个环形的项链上,有 颗能量珠。每颗珠子
有一个头标记
和一个尾标记
。相邻的两颗珠子
和
(其中
)可以合并,释放出
的能量,并形成一颗头标记为
、尾标记为
的新珠子。要求找出一种合并顺序,将所有珠子合并为一颗,使得释放的总能量最大。
输入数据为 个头标记,其中第
颗珠子的尾标记等于第
颗珠子的头标记(第
颗的尾标记等于第 1 颗的头标记)。
解题思路
本题是区间动态规划(Interval DP)在环形结构上的一个典型应用,与经典的矩阵链乘问题非常相似。
1. 问题转化:处理环形结构
环形结构给 DP 带来了困难,因为没有固定的起点和终点。一个常见的处理技巧是“断环成链”:我们将原来的 颗珠子序列复制一份接在后面,形成一个长度为
的链。这样,原来环上的任意连续
颗珠子,都对应于这个长链上的一个长度为
的子区间。
具体来说,如果输入的头标记序列是 ,我们构造一个新的标记数组
,其中包含
。这个新数组的长度为
。现在,问题就转化成了在这个链上求解所有长度为
的区间的最大合并能量,再从这些结果中取最大值。
2. 状态定义
我们定义 为将链上从第
颗到第
颗珠子合并成一颗所能释放的最大能量。我们的最终目标是
。
珠子 的头标记是
,尾标记是
。所以,合并珠子
到
这个区间,实际上是处理由标记点
定义的珠子序列。
3. 状态转移
我们考虑区间 的最后一次合并。这次合并必然是将一个已经合并好的子区间
和另一个子区间
合并而成的,其中
。
- 将区间
的珠子合并,能得到的最大能量是
。合并后的新珠子头标记为
,尾标记为
。
- 将区间
的珠子合并,能得到的最大能量是
。合并后的新珠子头标记为
,尾标记为
。
- 最后,将这两颗新珠子合并。根据能量释放规则,释放的能量为:第一颗珠子的头标记
第一颗珠子的尾标记
第二颗珠子的尾标记,即
。
综合三部分,通过分割点 得到的状态转移方程为:
4. 遍历顺序
与所有区间 DP 问题一样,我们按区间长度从小到大进行遍历。设区间长度为 ,从 2 遍历到
。对于每个长度,我们再遍历区间的起始点
。
从 2 到
。
从 1 到
。
。
从
到
。
最终,遍历所有可能的起点 (从 1 到
),在
中找到最大值即为答案。
代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> v(n);
for (int i = 0; i < n; ++i) {
cin >> v[i];
}
// 基础维度序列: v[0], v[1], ..., v[n-1], v[0]
vector<int> dims = v;
dims.push_back(v[0]);
// 用于DP的链式维度序列, 1-based indexing
vector<int> a(1, 0);
a.insert(a.end(), dims.begin(), dims.end());
// 追加 v[1]...v[n-1]
a.insert(a.end(), dims.begin() + 1, dims.end() - 1);
vector<vector<long long>> dp(a.size(), vector<long long>(a.size(), 0));
for (int len = 2; len <= n; ++len) {
// 链上有 2n-1 个珠子. 长度为 len 的链, 其起始位置 i 最大为 (2n-1) - len + 1 = 2n - len
for (int i = 1; i <= 2 * n - len; ++i) {
int j = i + len - 1;
for (int k = i; k < j; ++k) {
long long energy = (long long)a[i] * a[k + 1] * a[j + 1];
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + energy);
}
}
}
long long max_energy = 0;
for (int i = 1; i <= n; ++i) {
max_energy = max(max_energy, dp[i][i + n - 1]);
}
cout << max_energy << endl;
return 0;
}
import java.util.Scanner;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] v = new int[n];
for (int i = 0; i < n; i++) {
v[i] = sc.nextInt();
}
// 链式维度序列, 长度 2n, 采用 1-based indexing
int[] a = new int[2 * n + 1];
for(int i = 0; i < n; i++) {
a[i+1] = v[i];
}
a[n+1] = v[0];
for(int i = 1; i < n; i++) {
a[n+1+i] = v[i];
}
long[][] dp = new long[2 * n + 1][2 * n + 1];
for (int len = 2; len <= n; len++) {
// 链上有 2n-1 个珠子. 长度为 len 的链, 其起始位置 i 最大为 (2n-1) - len + 1 = 2n - len
for (int i = 1; i <= 2 * n - len; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) {
long energy = (long)a[i] * a[k + 1] * a[j + 1];
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k + 1][j] + energy);
}
}
}
long maxEnergy = 0;
for (int i = 1; i <= n; i++) {
maxEnergy = Math.max(maxEnergy, dp[i][i + n - 1]);
}
System.out.println(maxEnergy);
}
}
n = int(input())
v = list(map(int, input().split()))
# 构造链式维度序列 a,采用 1-based indexing
# 序列由 v[0]...v[n-1], v[0], v[1]...v[n-1] 构成
# 总长度为 2n+1
a = [0] * (2 * n + 1)
for i in range(n):
a[i + 1] = v[i]
a[n + 1] = v[0]
for i in range(1, n):
a[n + 1 + i] = v[i]
dp = [[0] * (2 * n + 1) for _ in range(2 * n + 1)]
# length 是区间内珠子的数量
for length in range(2, n + 1):
# i 是区间的起始珠子编号
# 链上有 2n-1 个珠子, 长度为 length 的链, 起始位置 i 最大为 (2n-1) - length + 1 = 2n - length
for i in range(1, 2 * n - length + 1):
j = i + length - 1
# k 是分割点, 将珠子链 [i, j] 分为 [i, k] 和 [k+1, j]
for k in range(i, j):
energy = a[i] * a[k + 1] * a[j + 1]
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j] + energy)
max_energy = 0
# 结果是所有长度为 n 的链中的最大值
for i in range(1, n + 1):
max_energy = max(max_energy, dp[i][i + n - 1])
print(max_energy)
算法及复杂度
- 算法:区间动态规划 (Interval Dynamic Programming),通过复制数组将环形问题转化为链式问题。
- 时间复杂度:
。处理加倍后的链需要三层循环:枚举区间长度
(
),枚举起始点
(
),枚举分割点
(
)。
- 空间复杂度:
。我们需要一个二维数组
来存储状态,其大小与
成正比。