一、题目理解
先来理解一下题意:\(1\ 3\ 5\ 2\) 这个例子,
\(A\)方案:
(1) 第一轮 \(1+3=4\) 变成 \(4\ 5\ 2\)
(2) 第二轮 \(4+5=9\) 变成 \(9\)
(3) 第三轮 \(9+2=11\)
最小代价:\(4+9+11=24\)
\(B\)方案:
(1) 第一轮 \(1+3=4\) 变成 \(4\ 5\ 2\)
(2) 第二轮 \(5+2=7\) 变成 \(4\ 7\)
(3) 第三轮 \(4+7\)=\(11\)
最小代价:\(4+7+11=22\)
可以看出,不同的组合方式,确实最终的代价是不一样的。
二、思考过程
1、尝试一维描述
万物皆\(dp\),一切状态都是由前面的状态转化而来。那么,如果用一维的方式来描述前面的状态,就是这样:\(f[i]\)。描述什么呢?似乎应该是从第\(1\)堆到第\(i\)堆的最小代价,如果能行的通,没有人会愿意把问题复杂化,再整什么二维概念了。
下面采用实例法描述一下最简问题,看看这样设计的状态描述办法会不会有问题:
-
如果有\(1\)堆石子,那么\(f[1]=0\)。为啥呢?一堆石子和谁合并啊,没的合并哪来的代价呢?
-
如果有\(2\)堆石子,那么\(f[2]=a[1]+a[2]\)。为啥呢?一共两堆,不合并它们两个还能合并谁,代价也没啥最不最小了,就是两堆石子的个数和。
-
如果有\(3\)堆石子,那么就有点意思了,可以\([(1)+(2)]+(3)\),也可以\((1)+[(2)+(3)]\),这时用\(f[i]\)怎么表示呢?我们发现,此时无法描述出\([(1)+(2)]\)和\([(2)+(3)]\),这是一个段的概念,一维的描述不出来!
2、尝试二维描述
现在来思考二维的描述法,比如\(f[i][j]\),描述就是从\(i\)堆到\(j\)堆的最小代价,依然是用最简例子思考一下:
-
如果有\(1\)堆石子,那么\(f[1][1]=0\)。
-
如果有\(2\)堆石子,那么\(f[1][2]=a[1]+a[2]\)。
-
如果有\(3\)堆石子,\([(1)+(2)]+(3)\),也可以\((1)+[(2)+(3)]\)
$f[1][3]=f[1][2]+f[2][3] + a[1]+a[2]+a[3] $
为啥最后要加上\(a[1]+a[2]+a[3]\)呢?因为最终两堆合并时,本轮产生的代价就是两堆的石子和啊。
三、状态转移方程
\(i\)和\(j\)区间内合并的最小代价,其实是在引入\(k\),其中\(i<=k<j\)后,考查每一个\(k\)作为中间结点来合并\(i\sim j\)的最小代价。
每一轮合并的代价增加值=区间内的石子数量。
如果每次都重新计算区间内的石子数量,性能太差,我们可以使用前缀和来优化。
\(f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1])\)
四、dfs实现代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
const int INF = 0x3f3f3f3f;
int a[N];
int f[N][N];
int dfs(int l, int r) {
int &v = f[l][r];
if (l == r) return v = 0;
if (v != INF) return v;
for (int i = l; i < r; i++)
v = min(v, dfs(l, i) + dfs(i + 1, r) + a[r] - a[l - 1]);
return v;
}
int main() {
int n;
cin >> n;
//前缀和
for (int i = 1; i <= n; i++) cin >> a[i], a[i] = a[i - 1] + a[i];
memset(f, 0x3f, sizeof(f));
cout << dfs(1, n) << endl;
return 0;
}
五、DP实现代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3010;
const int INF = 0x3f3f3f3f;
int n;
int s[N];//前缀和
int f[N][N];//表示将i到j合并成一堆的方案的集合,属性:最小值
// 石子合并
int main() {
cin >> n;
//一维前缀和,为了计算sum(i,j)的值
for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];
memset(f, 0x3f, sizeof f); //预求最小,先设最大
for (int i = 1; i <= n; i++)f[i][i] = 0;//第i堆与第i堆是不需要合并的,代价为0
// 长度为1的区间不用枚举,区间长度是1的话,表示只有一堆,一堆不需要合并,代价是0
for (int len = 2; len <= n; len++) //枚举区间长度
for (int i = 1; i + len - 1 <= n; i++) { //枚举区间左端点
int j = i + len - 1; //区间的右端点
//最后一个中间点K,需要枚举
for (int k = i; k < j; k++)
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
}
cout << f[1][n] << endl;
return 0;
}