问题描述
有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。
输入描述: 输入两行, 第一行为堆数n, 第二行为n堆石子的质量
输出描述: 输出最小代价
样例输入:
5
1 3 4 2 5
样例输出:
35
原因分析:
能否使用贪心算法求解?贪心算法过程如下(贪心思想为每次选取最小的组合合并):
显然结果不是我们想要的34, 那么得到最小值的合并过程是什么样的呢?过程如下:
实现过程:
包含的知识点: 前缀和, 区间DP, 枚举算法
假设一堆石子如下, 我们使用L和R代表左区间和右区间, 使用f[L, R]代表区间[L, R]中的最小代价, 为什么会想到使用f[L, R]这个来表示呢?因为我们表示状态是一个区间的状态, 区间是两个数(左右区间), 不能使用一个参数来代表.
因为想找出最小代价, 说明我们需要枚举出每一个区间的最小值, 在区间[L, R]中, 我们需要创建一个k标记, 来切割区间[L, R], 从L的位置开始, 一直到R-1的位置, 把区间[L, R](大问题)分解成区间[L, k]与[k+1, R]的总和(小问题), 实现大问题向小问题的转化, 其实这里还存在一个问题, [L, R]是分解成[L, k]与[k+1, R]了, 那么最后还需要将[L, k]与[k+1, R]这两堆石子进行合并, 所以需要用到前缀和公式, 得到如下的公式:
s[R] - s[L-1]将两堆石子进行合并的操作(前缀和). 当然有笨蛋喜欢使用for循环合并L到R区间的总和也可以, 不过会增加算法时间复杂度, 因为需要用到for循环
那么, 对于一个区间的最小代价, 状态转移方程为:
min()函数里面的f[L, R]会初始化为整型最大值, 其中k是切分点.
实现代码如下:
#include <iostream>
#include <cmath>
using namespace std;
int main(){
int n, r; // n代表堆数, r代表右区间
cin >> n;
int a[n+1], s[n+1] = {}; // a数组存储每个石子对应的质量, 数据存储下标1开始
// s数组存储前几个石子质量之和
int f[n+1][n+1]; // f代表区间的最小代价
// 数据的初始化
for( int i = 0; i < n+1; i++ ){ //f数组全赋值最大值
for( int j = 0; j < n+1; j++ ){
f[i][j] = INT_MAX;
}
}
for( int i = 1; i < n+1; i++ ){
cin >> a[i];
s[i] = s[i-1] + a[i];
f[i][i] = 0; // 当l与r相等时, 合并自己的代价为0
}
// 状态计算
for( int len = 2; len <= n; len++ ){ // 长度从2开始
for( int l = 1; l + len - 1 <= n; l++ ){ // 因为下标是1开始的, 左区间1开始
r = l + len - 1; // 通过l和len计算出r
for( int k = l; k < r; k++ ){ // k是切分点, 取值l开始, r-1结束
f[l][r] = min(f[l][r], f[l][k]+f[k+1][r]+s[r]-s[l-1]); // 状态转移方程
}
}
}
cout << f[1][n] << endl; // f[1, n]代表1到n的最小代价
return 0;
}
循环过程如下:
第一次循环:
第一层for循环: len = 2
第二层for循环: l = 1, r = 2
第三层for循环: k = 1
根据公式:
那么明显我们会选取到, 表达的意思即为两堆石子质量之和, 后续的f[i][j]会根据之前的得数递推出来.