石子合并问题
一、问题描述
【问题简述】
在一个园形操场的四周摆放n堆石子,现要将石子有次序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。试设计一个算法,计算出将n堆石子合成一堆的最小得分和最大得分。
【输入形式】
输入数据由文件名为input.txt的文本文件提供。文件的第1行为正整数n(1≤n≤100),表示有n堆石子;第二行有n个数,分别表示每堆石子的个数。
【输出形式】
将计算结果输出到文件output.txt。输出文件有两行,分别为得分最小的合并方案数值和得分最大合并方案数值。
二、问题分析
为了尽可能逼近目标的贪心法来逐次合并:从最上面的一堆开始,沿顺时针方向排成一个序列。 第一次选得分最小(最大)的相邻两堆合并,形成新的一堆;接下来,在 n-1 堆中选得分最小(最大)的相邻两堆合并……,依次类推, 直至所有石子经 n-1 次合并后形成一堆。
例如有6堆石子,每堆石子数(从最上面一堆数起,顺时针数)依次为
3 46 5 4 2 若要使得得分的总和最小。
按照贪心法,合并的过程如下:
第一次 3 4 6 5 4 2 -> 5
第二次 5 4 6 5 4 -> 9
第三次 9 6 5 4 -> 9
第四次9 6 9 -> 15
第五次 15 9 -> 24
总得分=5+9+9+15+24=62
但可得出另一个合并石子的方案:
第一次3 4 6 5 4 2 -> 7
第二次 7 6 5 4 2 -> 13
第三次 13 5 4 2 -> 6
第四次 13 5 6 -> 11
第五次 13 11 -> 24
总得分=7+13+6+11+24=61
显然,后者比贪心法得出的合并方案更优。
使用贪心法可能出错,是因为每一次选择得分最小(最大)的相邻两堆合并,不一定保证余下的合并过程能导致最优解。
故采用动态规划求解:如果N-1的全局最优解包含了每一次合并的子问题的最优解,那么经这样的N-1次合并后的得分总和必然是最优的。
三、算法设计
采用动态规划求解的关键是确定所有石子堆子序列的最佳合并方案。
这些石子堆子序列包括:
{第1堆、第2堆}{第2堆、第3堆}…{第N堆、第1堆};
{第1堆、第2堆、第3堆}{第2堆、第3堆、第4堆}…{第N堆、第1堆、第2堆};…
{第1堆、…、第N堆}{第2堆、…、第N堆、第1堆}…{第N堆、第1堆、…、第N-1堆}
为了便于运算,我们用〔i,j〕表示一个从第i堆数起,顺时针数j堆时的子序列{第i堆、第i+1堆、……、第(i+j-1)mod n堆}它的最佳合并方案包括两个信息:
①在该子序列的各堆石子合并成一堆的过程中,各次合并得分的总和;
②形成最佳得分和的子序列1和子序列2。由于两个子序列是相邻的,因此只需记住子序列1的堆数;
f〔i,j〕──将子序列〔i,j〕中的j堆石子合并成一堆的最佳得分和;
c〔i,j〕──将〔i,j〕一分为二,其中子序列1的堆数;(1≤i≤N,1≤j≤N)
显然,对每一堆石子来说,它的f〔i,1〕=0,c〔i,1〕=0(1≤i≤N)
对于子序列〔i,j〕来说,若求最小得分总和,f〔i,j〕的初始值为∞;若求最大得分总和,f〔i,j〕的初始值为0。(1≤i≤N,2≤j≤N)
动态规划的方向是顺推(即从上而下):
先考虑含二堆石子的N个子序列(各子序列分别从第1堆、第2堆、……、第N堆数起,顺时针数2堆)的合:
f〔1,2〕,f〔2,2〕,……,f〔N,2〕
c〔1,2〕,c〔2,2〕,……,c〔N,2〕
然后考虑含三堆石子的N个子序列(各子序列分别从第1堆、第2堆、……、第N堆数起,顺时针数3堆)的合并方案:
f〔1,3〕,f〔2,3〕,……,f〔N,3〕
c〔1,3〕,c〔2,3〕,……,c〔N,3〕
依次类推,直至考虑了含N堆石子的N个子序列(各子序列分别从第1堆、第2堆、……、第N堆数起,顺时针数N堆)的合并方案:
f〔1,N〕,f〔2,N〕,……,f〔N,N〕
c〔1,N〕,c〔2,N〕,……,c〔N,N〕
最后,在子序列〔1,N〕,〔2,N〕,……,〔N,N〕中,选择得分总和最小或最大。
四、关键代码
【初始化数组】
file.open("input.txt",ios::in); //打开输入文件
file>>n;
int A[2*n+1],B[2*n+1];
B[0]=0;
// 初始化数组
for (int i=1;i<=n;i++) {
file>>a;
A[i]=a;
A[i+n]=A[i];
}
// 计算最大和
for (int i=1;i<=2*n;i++) {
B[i]=B[i-1]+A[i];
}
file.close(); //关闭输入文件
【递归方程】
// 开始递归循环
for(int i=2*n-1;i>0;i--)
{
for(int j=i+1;j<i+n;j++)
{
Min[i][j] = INF; //#define INF 0x3f3f3f3f
for(int k=i;k<j;k++)
{
//这里相当于在第i堆与第j堆中间选择了第k堆作为中间值,在之前我们已经算好了最大和数组B[]
//Min[i][k]相当于在i,k中间取石子的最小值+M[k+1][j]表示在k+1,j之间取石子的最小值
//B[j]-B[i-1]就表示i-j石子加的和
Min[i][j]=min(Min[i][j],Min[i][k]+Min[k+1][j]+B[j]-B[i-1]);
Max[i][j]=max(Max[i][j],Max[i][k]+Max[k+1][j]+B[j]-B[i-1]);
}
}
}
【遍历找到最大与最小值】
int MaxNum=Max[1][n],MinNum=Min[1][n];
for (int i = 1; i <= n; i++) {
MaxNum=max(MaxNum,Max[i][i+n-1]);//得分最大
MinNum=min(MinNum,Min[i][i+n-1]);//得分最小
}
五、实验心得
【贪心法和动态规划法的比较】
动态规划法:
[基本思想与策略]
动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
[适用情况]
(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2)无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
贪心算法:
[基本思想与策略]
贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。贪心选择是采用从顶向下、以迭代的方法做出相继选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择的性质,我们必须证明每一步所作的贪心选择最终能得到问题的最优解。通常可以首先证明问题的一个整体最优解,是从贪心选择开始的,而且作了贪心选择后,原问题简化为一个规模更小的类似子问题。然后,用数学归纳法证明,通过每一步贪心选择,最终可得到问题的一个整体最优解。
[适用情况]
随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
最后,目标函数给出解的值。
【实验心得】
石子合并问题很容易把贪心法和动态规划法弄混,区分二者很简单,就是看题目是否包含二者的问题性质,贪心法需要具有最优子结构,且具有局部最优代替整体最优的性质,而动态规划算法一定要具最优子结构性质。
其次这个问题和我们之前遇到的凸多边形最优三角剖分和矩阵连乘非常类似。因为它们都是在找子问题、子结构。在不同的位置就问题分开成多个不同的子问题,对这些子问题求最优,然后从将子问题合并得到原问题,就达到了最优子结构的性质。
六、实验源码
#include<iostream>
#include<fstream>
#define INF 0x3f3f3f3f
using namespace std;
int Max[201][201],Min[201][201];
int main()
{
fstream file;
int n,a;
file.open("input.txt",ios::in); //打开输入文件
file>>n;
int A[2*n+2],B[2*n+2];
B[0]=0;
// 初始化数组
for (int i=1;i<=n;i++)
{
file>>a;
A[i]=a;
A[i+n]=A[i];
}
file.close(); //关闭输入文件
// 计算最大和
for (int i=1;i<=2*n;i++)
{
B[i]=B[i-1]+A[i];
}
// 开始递归循环
for(int i=2*n-1;i>0;i--)
{
for(int j=i+1;j<i+n;j++)
{
Min[i][j] = INF;
for(int k=i;k<j;k++)
{
//这里相当于在第i堆与第j堆中间选择了第k堆作为中间值,在之前我们已经算好了最大和数组B[]
//Min[i][k]相当于在i,k中间取石子的最小值+M[k+1][j]表示在k+1,j之间取石子的最小值
//B[j]-B[i-1]就表示i-j石子加的和
Min[i][j]=min(Min[i][j],Min[i][k]+Min[k+1][j]+B[j]-B[i-1]);
Max[i][j]=max(Max[i][j],Max[i][k]+Max[k+1][j]+B[j]-B[i-1]);
}
}
}
// 遍历找到最大与最小值
int MaxNum=Max[1][n],MinNum=Min[1][n];
for (int i = 1; i <= n; i++)
{
MaxNum=max(MaxNum,Max[i][i+n-1]);
MinNum=min(MinNum,Min[i][i+n-1]);
}
file.open("output.txt",ios::out); //打开输出文件
file<<MinNum<<endl<<MaxNum<<endl; //将结果输出到输出文件
file.close();
return 0;
}