算法实现题3-3石子合并问题
问题描述:在一个圆形操场的四周摆放着n堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2 堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分。
算法设计:对于给定n堆石子,计算合并成一堆的最小得分和最大得分。
数据输入:第一行是正整数n(1<=n<=100),表示有n堆石子。第2行有n个数,分别表示每堆石子的个数。
输入样例:
4
4 4 5 9
数据输出:第1行的数是最小得分,第2行中的数是最大得分
输出样例:
43
54
算法分析
这个问题和普通的石子合并问题不同,简单的石子合并问题采用的是将石子线性排列并合并,但是这个问题要求石子按照圆圈排列,所以首尾两个石子是能够合并的。
直接扩展数组,将两个一模一样的数组连接在一起,即环形转化为线性
4 4 5 9
转化为
4 4 5 9 4 4 5 9
首先,这道题要求我们只能将两个相邻的石子合并,许多人看到了这个例子就觉得应该使用贪心法来解决,每次合并最大的两个石子,就可以算出最大值。这是完全错误的,因为这个例子恰好四个石子是有序排列的,所以这个问题不具有局部最优代替整体最优的性质,不能使用贪心法。但是我们可以发现,这个问题确实具有子问题,子问题就是我们可以选择不同的堆进行合并,例如我们现在要对这四个石子合并,那我们可以选择各种不同的顺序进行合并,上面给的两种极值就是两种不同的合并方法,再比如我们可以(4+4)+(5+9)+(4+4+5+9)=44,即前两个合并,后两个合并,最后一起合并。
得出结论:问题虽然不具有局部最优代替整体最优的性质,但是其具有各种子问题。那我们自然而然可以想到使用动态规划法来解决这个。
动态规划强调的是状态和选择,所以我们先来分析题目中的状态:
首先状态一定有当前石子的数目,其次就是最后的得分。根据这两个状态我们就可以写出状态转移方程。
在写出状态转移方程之前,我们可以想到另一个很经典的矩阵连乘问题。我们需要寻找分割点,把多个相连的项分开。矩阵连乘我们找的是分割分割矩阵的位置(至少两个矩阵为1组,与本题目两两合并类似),所以本题我们参照这个问题的思路,找出一个分割点。在分割点前的石子合并,在分割点后的石子合并,最后将二块一起合并,听着还很像常见的归并算法(暂时这么理解吧,算法思路是一样的)。
所以我们的状态转移方程也是根据这个分割点来写的
Min[l][r]=min(Min[l][r],Min[l][k]+Min[k+1][r]+Sum[r]-Sum[l-1])
Max[l][r]=max(Max[l][r],Max[l][k]+Max[k+1][r]+Sum[r]-Sum[l-1])
逐一解释一下
Min[l][r]表示在l到r这些石子合并得到的最小得分
Max[l][r]表示在l到r这些石子合并得到的最大得分
k为我们的分割点,i<=k<j
sum[r]表示前r个石子的总和
sum[r]-sum[l-1]即为将最后两堆石子合并为最终结果时所得的分数
这么说起来可能有些晦涩,我们看图:
首先看sum数组,sum[l]就是当前l个石子的总和,sum[l]=Sum[l-1]+a[l]。
题中我们需要声明两个二维数组,一个是Min,一个Max(这里声明两个是为了将问题说得更清楚,其实声明一个就足够用了)。一个记录最小值,一个记录最大值。
下面我们来讨论一下数组的遍历顺序问题。从状态转移方程中我们可以看出k>=l恒成立,所以在二维矩阵中我们在计算当前行的Min[l][r]时总是需要其下面的某个值(因为用到了Min[k+1][r],k>=l,所以这个值一定在M[l][r]下面)。所以我们应该从下往上遍历,那么r应该如何遍历呢?
r恒为第l个石子后面的石子(因为需要合并),所以r应该在l+1到l+n之间遍历。
最终我们从最大值矩阵和最小值矩阵找出整体的最大值和最小值即可。下面来看看代码。
#include<bits/stdc++.h>
const int N=410, INF=0x3f3f3f3f;
using namespace std;
int main()
{
int n;
cin >> n;
int a[N]; //记录输入的每堆石子的数量
int sum[N]; //计算前缀和
int Min[N][N]; //表示把从l到r合并成一堆的得分总和最小值
int Max[N][N]; //表示把从l到r合并成一堆的得分总和最大值
memset(Min,INF,sizeof Min); //数组初始化
memset(Max,0,sizeof Max);
for(int i=1;i<=n;i++)
{
cin >> a[i];
a[i+n]=a[i]; //复制一遍区间
}
for(int i=1;i<=2*n;i++)
{
Min[i][i]=0; //初始化
Max[i][i]=0; //初始化
sum[i]=sum[i-1]+a[i]; //计算前缀和
}
for(int len=2;len<=n;len++) //阶段:枚举区间长度,len记录相邻合并的石子数
{
for(int l=1;l+len-1<=2*n;l++) //状态:枚举区间起点
{
int r=l+len-1; //区间终点
for(int k=l; k<r; k++) //决策:枚举分割点
{
Min[l][r]=min(Min[l][r],Min[l][k]+Min[k+1][r]+sum[r]-sum[l-1]);
Max[l][r]=max(Max[l][r],Max[l][k]+Max[k+1][r]+sum[r]-sum[l-1]);
}
}
}
int minn=INF, maxx=0;
for(int i=1;i<=n;i++)
{
minn=min(minn,Min[i][i+n-1]); //Min[1,n],f[2,n+1]..f[n,2n-1]
maxx=max(maxx,Max[i][i+n-1]); //Max[1,n],g[2,n+1]..g[n,2n-1]
}
cout << minn << endl << maxx;
return 0;
}