【题目描述】
将 n堆石子绕圆形操场排放,现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
1、选择一种合并石子的方案,使得做 n−1次合并得分总和最大。
2、选择一种合并石子的方案,使得做 n−1次合并得分总和最小。
【输入】
输入第一行一个整数 n,表示有 n 堆石子。
第二行 n 个整数,表示每堆石子的数量。
【输出】
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
【输入样例】
4
4 5 9 4
【输出样例】
43
54
【提示】
数据范围与提示:
对于 100% 的数据,有 1≤n≤200。
【解题思路】
使用区间动态规划的思路处理两个主要的任务:
-
状态定义:
- 我们定义了
dpMin[i][j]
和dpMax[i][j]
作为状态,其中i
和j
分别代表区间的起始和结束位置。dpMin[i][j]
表示将第i
堆到第j
堆石子合并成一堆所需要的最小得分,而dpMax[i][j]
则表示需要的最大得分。
- 我们定义了
-
状态转移:
- 状态转移方程考虑了所有可能的分割点
k
,将原问题分解为更小的子问题。对于dpMin
,我们寻找一个分割点k
,使得dpMin[i][k] + dpMin[k+1][j] + sum[j] - sum[i-1]
的值最小,这个值即为合并这个区间所需的最小得分。对于dpMax
,则是寻找使得总得分最大的分割点。 - 这里的
sum[j] - sum[i-1]
是用来计算从第i
堆到第j
堆石子的总数量,是合并这个区间的石子堆时所得到的得分。
- 状态转移方程考虑了所有可能的分割点
-
问题的环形特性处理:
- 由于问题是在环形结构中提出的,直接应用区间DP较为复杂。因此,通过将环形结构转化为线性结构(通过复制数组的方式),我们能够使用标准的区间DP方法来处理这个问题。这一步是这个问题解法中的一个巧妙之处。
解题步骤:
1. 模拟环形结构
因为石子堆是环形排列的,直接处理环形结构比较复杂。为了简化问题,我们将原始的n
堆石子复制一遍并拼接到原数组的后面,从而将环形结构转化为线性结构。这样做允许我们通过线性遍历的方式来模拟环形结构的所有可能的合并情况。
2. 使用前缀和优化区间和的计算
为了高效地计算任意一段区间内石子的总数,我们预先计算一个前缀和数组sum
。sum[i]
表示从第1堆石子到第i
堆石子的总数量。利用前缀和,我们可以在O(1)的时间内计算出任意一段区间的石子总数。
3. 动态规划求解
接下来,我们使用动态规划来分别求解合并得分总和的最小值和最大值。我们定义两个二维数组dpMin
和dpMax
:
dpMin[i][j]
表示从第i
堆到第j
堆石子合并成一堆所需的最小得分。dpMax[i][j]
表示从第i
堆到第j
堆石子合并成一堆所需的最大得分。
动态规划的状态转移方程为:
- 对于最小得分:
dpMin[i][j] = min(dpMin[i][j], dpMin[i][k] + dpMin[k+1][j] + sum[j] - sum[i-1])
- 对于最大得分:
dpMax[i][j] = max(dpMax[i][j], dpMax[i][k] + dpMax[k+1][j] + sum[j] - sum[i-1])
这里,k
是区间[i, j]
内的一个分割点,我们尝试每一种可能的分割方式来更新当前区间的最小和最大得分。
4. 遍历所有可能的起点
因为原问题是在环形结构上的,每堆石子都可以作为合并的起点。我们通过遍历每一个可能的起点(即每个i
),并计算以该起点开始的长度为n
的区间的最小得分和最大得分。最终,我们从所有这些起点中找到最小得分和最大得分。
5. 输出结果
最后,代码输出所有可能起点中计算得到的最小得分和最大得分。
通过上述步骤,代码有效地解决了环形石子合并问题,既考虑了环形结构的特殊性,又通过动态规划的方法优化了计算过程,实现了对合并得分总和最小值和最大值的求解。
【代码实现】
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 200 + 5; // 增大数组大小,以便处理原始数组和复制的数组
int stones[MAXN * 2]; // 存储石子数量,数组大小翻倍以模拟环形结构
int dpMin[MAXN][MAXN]; // dpMin[i][j]表示将第i堆到第j堆石子合并成一堆的最小得分
int dpMax[MAXN][MAXN]; // dpMax[i][j]表示将第i堆到第j堆石子合并成一堆的最大得分
int sum[MAXN * 2]; // 前缀和数组,用于快速计算区间和
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> stones[i];
stones[i + n] = stones[i]; // 将原数组复制一遍到数组后半部分以模拟环形
}
// 计算前缀和,用于快速获取任意一段区间的石子总数
for (int i = 1; i <= 2 * n; ++i) {
sum[i] = sum[i - 1] + stones[i];
}
// 初始化DP数组,对角线上的值为0,因为单个堆不需要合并
memset(dpMin, 0x3f, sizeof(dpMin));
memset(dpMax, 0, sizeof(dpMax));
for (int i = 1; i <= 2 * n; i++) dpMin[i][i] = dpMax[i][i] = 0;
// 动态规划计算最小得分和最大得分
for (int len = 2; len <= n; ++len) { // 从长度为2的区间开始计算,直到整个环
for (int i = 1; i + len - 1 <= 2 * n; ++i) {
int j = i + len - 1;
for (int k = i; k < j; ++k) {
// 更新当前区间的最小得分和最大得分
dpMin[i][j] = min(dpMin[i][j], dpMin[i][k] + dpMin[k + 1][j] + sum[j] - sum[i - 1]);
dpMax[i][j] = max(dpMax[i][j], dpMax[i][k] + dpMax[k + 1][j] + sum[j] - sum[i - 1]);
}
}
}
// 在所有可能的起点中找到最小得分和最大得分
int minScore = INT_MAX, maxScore = 0;
for (int i = 1; i <= n; ++i) { // 因为环形的性质,需要考虑从每个位置开始的情况
minScore = min(minScore, dpMin[i][i + n - 1]);
maxScore = max(maxScore, dpMax[i][i + n - 1]);
}
cout << minScore << endl; // 输出最小得分
cout << maxScore << endl; // 输出最大得分
return 0;
}