这是经典的区间 dp \text{dp} dp 例题。
读者可以在 洛谷 P1775 上提交。
题目大意
设有 N ( N ≤ 300 ) N(N \le 300) N(N≤300) 堆石子排成一排,其编号为 1 , 2 , 3 , ⋯ , N 1,2,3,\cdots,N 1,2,3,⋯,N。每堆石子有一定的质量 m i ( m i ≤ 1000 ) m_i(m_i \le 1000) mi(mi≤1000)。现在要将这 N N N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。
分析
我们定义 f i , j f_{i,j} fi,j 为合并第 i i i 堆到第 j j j 堆的最小花费。
我们使用自底向上递推的方法,现在小区间内得到最优解,然后再合并这些小区间,得到整体的最优解。
当我们计算整个区间 [ i , j ] [i,j] [i,j] 的最优值时,合并两个枚举出来的小区间 [ i , k ] [i,k] [i,k] 和 [ k + 1 , j ] [k+1,j] [k+1,j],对所有可能的合并( i ≤ k < j i \le k<j i≤k<j,即在 i i i 和 j j j 之间滑动),只采用最优的合并方案。
转移方程为:
f i , j = min { f i , k + f k + 1 , j + w i , j } \Large f_{i,j}=\min\{f_{i,k}+f_{k+1,j}+w_{i,j}\} fi,j=min{fi,k+fk+1,j+wi,j}
那么答案就是 f 1 , n f_{1,n} f1,n,也就是合并第 1 1 1 堆到第 n n n 堆的最小花费。
其中 w i , j w_{i,j} wi,j 代表从第 i i i 堆一直到第 j j j 堆石子总数。换句话说,第 i i i 堆合并到第 j j j 堆的价值。
求
w
i
,
j
w_{i,j}
wi,j 的时候引用差分的思想,求区间和可以转化为求 s[r]-s[l-1]
。
下面是包含 i , j , k i,j,k i,j,k 的三重循环,时间复杂度为 O ( n 3 ) \mathcal{O}(n^3) O(n3)。
- 第 1 1 1 个循环枚举 j j j,也就是当前大区间的终点。
- 第 2 2 2 个循环枚举 i i i,也就是当前大区间的起点。
- 第 3 3 3 个循环枚举 k k k,在当前大区间内滑动的小区间。
注意:起点 i i i 应该从 j − 1 j-1 j−1 开始递减,也就是从最小的区间 [ j − 1 , j ] [j-1,j] [j−1,j] 开始一直到 [ 1 , j ] [1,j] [1,j]。
为什么不能正着递增呢?因为那样就是从大区间到小区间,打擂台的时候会出现问题。
我们又发现 a i a_i ai 已经被前缀和代替了,已经没有意义,所以用临时变量 x x x 代替。
#include<bits/stdc++.h>
using namespace std;
int n,x,f[301][301],w[301][301],s[301];
signed main() {
cin>>n;
for(int i = 1;i<=n;i++) {
cin>>x;
s[i] = s[i-1]+x;//s 数组为前缀和数组
}
for(int i = 1;i<=n;i++){
for(int j = 1;j<=n;j++){
w[i][j] = s[j]-s[i-1];//第 i 堆一直到第 j 堆的石子总数,更直观
}
}
for(int j = 2;j<=n;j++){
for(int i = j-1;i>=1;i--){
f[i][j] = INT_MAX;//因为要取最小方案,所以初值为无穷大。
for(int k = i;k<j;k++){
f[i][j] = min(f[i][j],f[i][k]+f[k+1][j]+w[i][j]);//转移方程
}
}
}
cout<<f[1][n]<<endl;//答案
return 0;
}
当然转移方程这里可以设一个辅助变量 len
代表区间的长度。
所以核心代码应该改成这样:
for(int len = 2; len<=n; len++) {//枚举长度
for(int i = 1; i<=n-len+1; i++) {//先枚举起点
int j = i+len-1;//通过起点和长度推出终点
f[i][j] = INT_MAX;
for(int k = i; k<j; k++) {//这里就和之前一样了。
dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);
}
}
}
第二种解法,这里考虑记忆化。
dfs ( l , r ) \text{dfs}(l,r) dfs(l,r) 代表 [ l , r ] [l,r] [l,r] 区间的最小价值。
那我们就枚举 [ l , r ] [l,r] [l,r] 区间中的每一个石子,给每个小区间打擂台。
其他部分的内容和上面的动规一样。
#include <bits/stdc++.h>
using namespace std;
int a[301],f[301][301],n,s[301],w[301][301];
int dfs(int l,int r) {
if (l==r) return 0;//如果头尾相同那就停止
if (f[l][r]!=0x3f3f3f3f) return f[l][r];//返回答案
for (int i = l; i<r; i++) {
f[l][r] = min(f[l][r],dfs(l,i)+dfs(i+1,r)+w[l][r]);//方程
}
return f[l][r];//返回答案
}
int main() {
cin>>n;
memset(f,0x3f,sizeof f);//设初值
/*后面内容和前面一样的。*/
for(int i = 1; i<=n; i++) {
cin>>a[i];
s[i] = s[i-1]+a[i];
}
for(int i = 1; i<=n; i++) {
for(int j = 1; j<=n; j++) {
w[i][j] = s[j]-s[i-1];
}
}
cout<<dfs(1,n)<<endl;//f[1][n] 和 dfs(1,n) 意义相同,都为合并第 1 堆到第 n 堆的最小花费。
return 0;
}