题目地址:https://www.rqnoj.cn/problem/490
在一个圆形操场的四周摆放n堆石子(n≤500),现要将石子有次序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
编一程序,由文件读入堆数n及每堆的石子数;
⑴选择一种合并石子的方案,使得做n-1次合并,得分的总和最小;
⑵选择一种合并石子的方案,使得做n-1次合并,得分的总和最大。
输入文件:
第1行为n,代表n堆石子;
第2行为n个数,代表每堆石子的数量。
输出文件:
第1行为合并的得分总和最小值;
第2行为合并的得分总和最大值。
输入样例:
4
4 5 9 4(从最上面的一堆数起,顺时针数)
输出样例:
43(最小)
54(最大)
【分析】
第一次拿到该题时,很多选手会采用尽可能逼近目标的贪心法来逐次合并。如样例,从最上面一堆开始,沿顺时针方向排成一个环,要计算得分最小时,第一次取相邻得分最小的4,4两堆合并,得分为8分,合并后剩下3堆,每堆的分数为8,9,5;再选相邻得分最小的5,8合并,得13分,最后选9和13两堆合并,得22分,总共分数为43,与输出一样,好像这种贪心策略是对的。其实不然,这种策略存在反例。
下面我们举一个贪心算法不能正确解答的例子:
例如6堆石子,从最上面一堆顺时针数起,钟表12点位置是第一堆,6点位置为第4堆.....共六堆石子(如图2-1中的合并前)
第1堆石子数为3,第2堆石子数为4,第3堆石子数为6,第4堆石子数为5,第5堆石子数为4,第6堆石子数为2,采用贪心策略合并时,合并过程如图2-1所示。
很显然,图2-2中的合并方案比图2-1中的贪心合并方案更优。样例中造就了一种用贪心策略可以解决问题的假象,导致很多竞赛选手使用了贪心策略,从而丢失了很多分。下面来分析第二种合并方案。从后往前推,由图2-2可以看出,6堆石子合并的最小得分方案min{merge(①,②,③,④,⑤,⑥)}是由merge(①,②,③)+merge(④,⑤,⑥)得来的,上述中,merge表示合并。merge(①,②,③)时,它有两种合并方案,即先merge(①,②)两堆,再将①②合并的结果与第③堆合并,或先merge(②,③),再将②③合并的结果与第①堆合并。两种方案的合并过程如下:
第一种合并方案:(3+4)+6合并得分为7+13=20;
第二种合并方案:3+(4+6)合并得分为10+13=23。
两种方案相比,明显第一种方案得分小。merge(④,⑤,⑥)时,同样的,也有两种方案,即先merge④,⑤两堆,再将④⑤合并的结果与第⑥堆合并,或merge⑤,⑥,再将⑤⑥合并的结果与第④堆合并。两种方案的合并过程如下:
第一种合并方案:(5+4)+2合并得分为9+11=20;
第二种合并方案:5+(4+2)合并得分为6+11=17。
这两种方案相比,明显第二种方案得分小。从图2-2中的最小合并得分结果来看,在计算6堆石子合并的最小得分min{merge(①,②,③,④,⑤,⑥)}时,它的计算过程明显是取min{merge(①,②,③)}和min{merge(④,⑤,⑥)}的。由此可以初步看出,合并石子时具备最优子结构的性质。详细看合并1到6堆时存在的过程。
合并1堆:①,②,…,⑥;
合并2堆:①②,②③,…,⑥①;
合并3堆:①②③,②③④,…,⑥①②;
合并4堆:①②③④,②③④⑤,…,⑥①②③;
合并5堆:①②③④⑤,②③④⑤⑥,…,⑥①②③④;
合并6堆:①②③④⑤⑥,②③④⑤⑥①,…,⑥①②③④⑤。
有合并过程可以看出,从第i堆开始合并j堆时,它的值可以为第i堆+min{merge(i+1,i+2,…,i+j-1)}。如从第①堆开始合并4堆时,它可以为第①堆+min{merge(②,③,④)},可以为min{merge(①,②)}+min{merge(③,④)},可以为min{merge(①,②,③)}+第④堆,共有3种来源,与区间的合并有关。以此类推,合并到6堆时,取从第i堆开始合并6堆的最小值,即得到合并的总的最小值。所以,我们可以肯定地说,此问题具备最优子结构的性质,而且无后效性。
用合并的堆数作阶段,用f[i,j]作状态,表示从第i堆数起,顺时针合并j堆的总得分最小值,它包括合并前j-1堆的最小总得分加上这次合并的得分,用sum[i,j]表示这次合并的得分。合并时的堆数可以表示为序列{第i堆,第i+1堆,…,第(i+j-2) mod n+1堆}。序列总得来的方案有很多种,我们用子序列1和子序列2表示,如子序列1为{第i堆},则子序列2为{第i+1堆,…,第(i+j-2)mod n+1堆}。子序列1和子序列2相邻,所以,假如子序列1为k堆,则子序列2为j-k堆。由此,可以得到动规方程:
f[i,j]=min{f[i,k]+f[i+k,j-k]+sum[i,j],1≤k≤j-1}
用stone[i]表示初始时每堆的石子数,则动规的初始条件为:
f[i,1]=0
#include
using namespace std;
const int maxn=99999999;
int num[510];
int sum[510][510];//这一次合并得到的分数
int fmax[510][510];//从第i堆石子开始,合并j堆石子得到的最大值
int fmin[510][510];//从第i堆石子开始,合并j堆石子得到的最小值
int main()
{
int n,i,j;
scanf("%d",&n);
for(i=1;i<=n;++i)
{
scanf("%d",&num[i]);
sum[i][1]=num[i];
fmax[i][1]=0;
fmin[i][1]=0;
}
for(j=2;j<=n;++j)
for(i=1;i<=n;++i)
sum[i][j]=num[i]+sum[(i%n)+1][j-1];//转一圈,相当于从谁开始合并都考虑了。
for(j=2;j<=n;++j)
{
for(i=1;i<=n;++i)
{
fmax[i][j]=0;
fmin[i][j]=maxn;
for(int k=1;k<=j-1;++k)
{
int next=((i+k-1)%n)+1;
if(fmax[i][j]sum[i][j]+fmin[next][j-k]+fmin[i][k])
fmin[i][j]=sum[i][j]+fmin[next][j-k]+fmin[i][k];
}
}
}
//哪个位置当第一次合并
int min=maxn ,max=0;
for(i=1;i<=n;++i)
{
if(min>fmin[i][n])
min=fmin[i][n];
if(max