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

题目描述:

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

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

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

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


解题思路:

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

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


方法一:

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

然后是第一种方法的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;
} 

为什么要枚举一个叫做 k k k 的分割点呢?其实可以把一个区间理解为一大堆分散的石子,现在要把它合并成一堆,因此我们要不断的枚举他是由原来的怎样的两堆石子构成的,要让这两个子区间合并后的和最小,这就是区间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;
}

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


方法三:

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

d p i , j = d p i , k + d p i + k , j − k + s u m i , i + j − 1 dp_{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

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

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

#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算法,需要花时间琢磨

猜你喜欢

转载自blog.csdn.net/SAI_2021/article/details/119834927