SSL2863 石子合并 (动态规划 区间DP)

该博客介绍了如何运用动态规划解决将N堆石子合并成一堆的最小花费问题。通过三种不同的方法,包括枚举区间分割点,枚举区间长度和改变dp数组状态描述,详细阐述了状态转移方程和代码实现,强调了区间DP在解决问题中的应用。

题目描述:

有N堆石子,现要将石子有序的合并成一堆,规定如下:

  • 每次只能移动相邻的2堆石子合并,合并花费为新合成的一堆石子的数量。

求将这N堆石子合并成一堆的总花费最小
例如:
4
8 2 3 6

方案一代价为:10+9+19=3810+9+19=3810+9+19=38
方案二代价为:5+11+19=355+11+19=355+11+19=35
所以最小代价为 353535


解题思路:

动态规划常常采取从部分整体最优解的拆分来得到最优解法的递归式,我们可以想到,此处是由2堆石子合并,所以最终最优解肯定是由两个局部最优解的加上整体的和求得。

看到这种的题目,我们可以考虑到用区间DP来维护最小代价。
这题的做法比较多样性,可以有三种做法可以完成:


方法一:

首先设定状态,dpi,jdp_{i,j}dpi,j 表示从第 iii 块石子到第 jjj 块石子的区间合并的最小代价
通过观察题目可以知道,一个最小代价的石子堆是由两个最小代价的石子堆合并而成的,符合最优子结构性,同时,也证实了可以用区间DP来做。
状态转移方程很快就能推出来:
在这里插入图片描述
其中sumi,jsum_{i,j}sumi,j 表示 iiijjj 的和,可以理解为整体的和。

然后是第一种方法的code:
先枚举石子区间的左端点,再枚举右端点,最后枚举区间分割点。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,a[110],sum[110];
int dp[110][110]={0};
void input()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	  {
	  	cin>>a[i];
	  	sum[i]=sum[i-1]+a[i];  //用一维前缀和代替二维的
	  	
	  }
}

void DP()
{
	for(int i=n;i>=1;i--)  //枚举左端点
	  {
	  	for(int j=i+1;j<=n;j++)  //枚举右端点
	  	  {
	  	     for(int k=i;k<=j-1;k++)   //枚举区间中的分割点
			   {
			   	    if(dp[i][j]!=0) dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
			   	    //将区间一分为二合并找和最小的方案,可以理解为两堆石子合并的过程
			   	    else dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
			   }	
		  }
	  }
	cout<<dp[1][n];
}

int main()
{  
   input();
   DP();
   return 0;
} 

为什么要枚举一个叫做 kkk 的分割点呢?其实可以把一个区间理解为一大堆分散的石子,现在要把它合并成一堆,因此我们要不断的枚举他是由原来的怎样的两堆石子构成的,要让这两个子区间合并后的和最小,这就是区间DP的思想。


方法二:

状态还是那种状态,转移方程还是那个转移方程,依旧是区间DP,只是写法不同了而已。

先枚举单个区间的长度,再枚举左端点,最后枚举分割点

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,a[110],sum[110];
int dp[110][110]={0};
void input()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	  {
	  	cin>>a[i];
	  	sum[i]=sum[i-1]+a[i];
	  	
	  }
}

void DP()
{
	for(int len=2;len<=n;len++)  //此区间长度
	  {
	  	for(int i=1;i<=n-len+1;i++)  //左端点
	  	  {
	  	    int j=i+len-1;
			dp[i][j]=0x3f3f3f3f;
			for(int k=i;k<=j-1;k++)	
			  {
			  	dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
			  }
		  }
	  }
	cout<<dp[1][n];
}

int main()
{  
   input();
   DP();
   return 0;
}

枚举长度作为主循环,可以理解为从最短的区间长度(从底层人民开始),一层一层的往上推。


方法三:

这里对 dpdpdp 数组的状态描述要改一下:
dpi,jdp_{i,j}dpi,j 表示以 iii 为左端点,往后的 jjj 个数所组成的区间的最小代价。
由于 jjj 所描述的意义不同了,因此区间DP的状态转移方程也得改一下:

dpi,j=dpi,k+dpi+k,j−k+sumi,i+j−1dp_{i,j}=dp_{i,k}+dp_{i+k,j-k}+sum_{i,i+j-1}dpi,j=dpi,k+dpi+k,jk+sumi,i+j1

kkk 依旧是描述两堆石子的分割点。
因此代码的书写方法是这样的:

先枚举左端点,然后枚举区间长度,最后枚举分割点

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,a[110],sum[110];
int dp[110][110]={0};
void input()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	  {
	  	cin>>a[i];
	  	sum[i]=sum[i-1]+a[i];
	  	
	  }
}

void DP()
{
	for(int j=2;j<=n;j++)
	  {
	  	for(int i=1;i<=n-1;i++)
	  	  {
	  	    dp[i][j]=0x3f3f3f3f;
			for(int k=1;k<=j-1;k++)
			  dp[i][j]=min(dp[i][j],dp[i][k]+dp[i+k][j-k]+sum[i+j-1]-sum[i-1]);	
		  }                                               //这里sum的运算表示求区间和
	  }
	cout<<dp[1][n];
}

int main()
{  
   input();
   DP();
   return 0;
} 

总结:

区间DP是一个神奇的DP算法,需要花时间琢磨

评论 20
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值