石子合并

石子合并

设有 $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/

posted @ 2022-08-31 21:30  onlyblues  阅读(281)  评论(0编辑  收藏  举报
Web Analytics