1. 问题描述:
设有 N 堆石子排成一排,其编号为 1,2,3,…,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 ≤ N ≤ 300
输入样例:
4
1 3 5 2
输出样例:
22
来源:https://www.acwing.com/problem/content/description/284/
2. 思路分析:
分析题目可以知道题目中存在一个限制我们必须合并的是相邻的堆,所以在合并石子的过程中可以将石子看成是区间的端点,由这些石子构成了构成了一段长度为n的区间,在合并相邻的石子的过程中这些区间是连续的,所以属于经典的区间dp的问题,区间dp一般声明一个二维的数组或者二维列表,其中dp[i][j]表示区间[i, j]对应的最优值(区间dp的一般套路),对于这道题目来说dp[i][j]表示合并区间[i,j]石子的最小代价。区间dp一般都是类似的套路,需要使用三层的循环,第一层循环表示当前区间的长度l,第二层循环表示当前以i为起点长度为l的区间,第三层循环表示当前区间[i,j]的各个连续的端点。分析题目可以知道我们需要先从小的区间递推出大的区间,也即先求解出长度较小的区间合并石子的最小代价,然后递推出长度较大的区间合并石子的最小代价。对于每一段长度为l的区间我们需要尝试所有可能的合并方案,也即枚举当前区间中的所有端点,以当前的端点为分界线尝试求解合并当前区间石子的最小代价,当枚举出区间[i, j]中的所有端点之后那么区间[i,j]合并石子的最小代价就求解出来了(在合并两堆相邻石子的过程中的操作是独立的所以对于这两堆石子对应区间的求解最小代价相加即可),区间dp本质上还是枚举所有长度的区间并且由小的区间递推出大的区间,可以看成是暴力枚举的优化。
3. 代码如下:
import sys
if __name__ == '__main__':
n = int(input())
s = list(map(int, input().split()))
s.insert(0, 0)
dp = [[0] * (n + 1) for i in range(n + 1)]
# 前缀和列表用来计算[i,j]石子的价值, 在合并区间[i,j]的操作中最终[i,j]区间肯定是包含了[i,j]区间的石子价值所有在递推的过程中需要加上前缀和
for i in range(1, n + 1):
s[i] += s[i - 1]
for l in range(2, n + 1):
i = 1
j = i + l - 1
# 枚举所有长度为l起点为i的区间
while j <= n:
dp[i][j] = sys.maxsize
# 枚举区间[i:j]的所有端点其实是在枚举所有可能的合并方案
for k in range(i, j):
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + s[j] - s[i - 1])
i += 1
j = i + l - 1
print(dp[1][n])