石子合并
石子合并
设有 $N$ 堆石子排成一排,其编号为 $1,2,3, \dots ,N$。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 $N$ 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 $4$ 堆石子分别为 1 3 5 2 , 我们可以先合并 $1$、$2$ 堆,代价为 $4$,得到 4 5 2 , 又合并 $1$,$2$ 堆,代价为 $9$,得到 9 2 ,再合并得到 $11$,总代价为 $4+9+11=24$;
如果第二步是先合并 $2$,$3$ 堆,则代价为 $7$,得到 4 7 ,最后一次合并代价为 $11$,总代价为 $4+7+11=22$。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 $N$。
第二行 $N$ 个数,表示每堆石子的质量(均不超过 $1000$)。
输出格式
输出一个整数,表示最小代价。
数据范围
$1 \leq N \leq 300$
输入样例:
4 1 3 5 2
输出样例:
22
解题思路
区间dp。之前一直不知道什么时候用区间dp,经过一段时间后现在慢慢有点思路了。
什么时候可以用区间dp呢?当一个问题可以分成两个部分(也可以是多个部分,下面都以两个部分的情况为例)来求解得到,且这两个部分的求解是相互独立的,也就是求解这一部分的结果并不会对另一部分的结果产生影响。同时这两个部分可以看作是相对于原问题规模更小的问题。因此如果要求整个问题的最大值,那么就分别求这两个部分的最大值,原问题的答案就是这两个部分的最大值之和。
因此区间dp可以理解为分治加上记忆化搜索。
以这一题为例,原问题就是求将$1 \sim n$的石子合并成一堆所需要的最小代价。每一次的合并都是将两堆石子合并成一堆,这启示我们一开始可以把整堆石子分成两大堆,根据两堆石子的边界进行划分,得到下面的集合划分:
一共有$n-1$种划分的方式。并且任意两堆之间的求解是相互独立的。可以发现要合并$1 \sim n$这$n$堆石子,就是把划分得到的两大堆石子合并,这也是合并的最后一步,就会得到将所有石子合并成一堆的结果。因此答案就是枚举每个集合,看一下哪种划分方式得到的答案最小。那么如何求每个集合的最小值呢?与合并$n$堆石子一样,对这两堆的石子分别用同样的方式划分成两堆,然后枚举各个集合取最小值。
定义状态$f(i,j)$表示所有将第$i$堆石子到第$j$堆石子合并成一堆石子的合并方式的集合。根据两堆石子的边界进行集合划分,因此状态转移方程就是$$f(i, j) = \min_{i \leq k < j} \{ {f(i,k) + f(k + 1, j) + s_{j} - s_{i - 1}} \}$$
AC代码如下:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 310; 5 6 int s[N]; 7 int f[N][N]; 8 9 int dp(int l, int r) { 10 if (f[l][r] != 0x3f3f3f3f) return f[l][r]; 11 if (l == r) return f[l][r] = 0; 12 13 for (int k = l; k < r; k++) { 14 f[l][r] = min(f[l][r], dp(l, k) + dp(k + 1, r) + s[r] - s[l - 1]); 15 } 16 17 return f[l][r]; 18 } 19 20 int main() { 21 int n; 22 scanf("%d", &n); 23 for (int i = 1; i <= n; i++) { 24 scanf("%d", s + i); 25 s[i] += s[i - 1]; 26 } 27 28 memset(f, 0x3f, sizeof(f)); 29 30 printf("%d", dp(1, n)); 31 32 return 0; 33 }
上面的代码是记忆化搜索来实现的,更为直观。也可以写成循环迭代的:
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int N = 310; 5 6 int s[N]; 7 int f[N][N]; 8 9 int main() { 10 int n; 11 scanf("%d", &n); 12 for (int i = 1; i <= n; i++) { 13 scanf("%d", s + i); 14 s[i] += s[i - 1]; 15 } 16 17 memset(f, 0x3f, sizeof(f)); 18 for (int len = 1; len <= n; len++) { 19 for (int i = 1; i + len - 1 <= n; i++) { 20 int j = i + len - 1; 21 if (len == 1) { // 区间长度为1的情况要特判 22 f[i][j] = 0; 23 } 24 else { 25 for (int k = i; k < j; k++) { 26 f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]); 27 } 28 } 29 } 30 } 31 32 printf("%d", f[1][n]); 33 34 return 0; 35 }
参考资料
AcWing 282. 石子合并(算法基础课):https://www.acwing.com/video/319/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/16644542.html