石子合并问题:有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。

这是一道典型的动态规划题目。令dp[i][j]为将第i到j堆石子合并成一堆的最小代价w[i][j]为第i到j堆石子的总数。就可以写出状态转移方程:

直接暴力求解就可以,复杂度O(n3)。

改进方法是用平行四边形不等式优化,缩小k的范围,复杂度O(n2) :算法学习笔记(79): 四边形不等式优化DP - 知乎 (zhihu.com)

问题的变种是让石子环形排列,即第一堆和最后一堆看成是相邻的。解决方法是在n堆石子后面再加上与前面一样的n堆石子,这样最后一堆和第一堆就相邻了。把总堆数看成2n,用上面的方法解出来就可以了。最后还需要遍历一下dp数组,求出2n堆石子里面结果最小的n堆石子作为最终结果。

P1880 [NOI1995] 石子合并 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <algorithm>
#include <iostream>
#include <vector>
#define INF 0x7fffffff
using namespace std;
int main() {
int n;
cin >> n;
vector<int> stones(2 * n + 1);
vector<vector<int>> dp(2 * n + 1, vector<int>(2 * n + 1));
vector<vector<int>> s(2 * n + 1, vector<int>(2 * n + 1));
vector<vector<int>> dp1(2 * n + 1, vector<int>(2 * n + 1));
vector<int> sum(2 * n + 1, 0);
for (int i = 1; i <= n; i++) {
cin >> stones[i];
stones[n + i] = stones[i];
sum[i] = sum[i - 1] + stones[i];
}
for (int i = n + 1; i <= 2 * n; i++) {
sum[i] = sum[i - 1] + stones[i];
}

for (int r = 0; r < 2 * n; r++) {
for (int i = 1; i <= 2 * n; i++) {
int j = i + r;
if (j > 2 * n) break;
if (i == j) {
dp[i][i] = 0, dp1[i][i] = 0, s[i][i] = i;
continue;
}
dp[i][j] = INF;
dp1[i][j] = 0;
for (int k = s[i][j - 1]; k <= min(j - 1, s[i + 1][j]); k++) {
if (dp[i][j] > dp[i][k] + dp[k + 1][j]) {
dp[i][j] = dp[i][k] + dp[k + 1][j];
s[i][j] = k;
}
}
for (int k = i; k < j; k++) {
dp1[i][j] = max(dp1[i][j], dp1[i][k] + dp1[k + 1][j]);
}
dp[i][j] += (sum[j] - sum[i - 1]);
dp1[i][j] += (sum[j] - sum[i - 1]);
}
}
int re1 = INF;
int re2 = 0;
for (int i = 1; i <= n; i++) {
re1 = min(re1, dp[i][i + n - 1]);
re2 = max(re2, dp1[i][i + n - 1]);
}
printf("%d\n%d", re1, re2);
}