石子合并——经典区间 dp 习题题解。

这是经典的区间 dp \text{dp} dp 例题。

读者可以在 洛谷 P1775 上提交。

题目大意

设有 N ( N ≤ 300 ) N(N \le 300) N(N300) 堆石子排成一排,其编号为 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(mi1000)。现在要将这 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 ik<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 j1 开始递减,也就是从最小的区间 [ j − 1 , j ] [j-1,j] [j1,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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值