问题描述:
在一个圆形操场的四周摆放着 n 堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的 2 堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。试设计一个算法,计算出将 n 堆石子合并成一堆的最小得分和最大得分。
设计任务
对于给定 n 堆石子,编程计算合并成一堆的最小得分和最大得分。
数据输入
由文件 input.txt 提供输入数据。文件的第 1 行是正整数 n ,表示有 n 堆石子。第二行有 n 个数,分别表示每堆石子的个数。
结果输出:
程序运行结束时,将计算结果输出到文件 output.txt 中。文件的第 1 行中的数是最小得分;第 2 行中的数是最大得分。
问题分析
由题意知,该题为动态规划经典题型,但和普通的石子合并问题不同,简单的石子合并问题采用的是将石子线性排列并合并,但是这个问题要求石子按照圆圈排列,所以首尾两个石子是能够合并的,且合并方向不明确。
在线性问题中,可将1,2,3三堆石子顺序合并起来,然而在环形排列中存在两种情况。一是:合并{(1,2),3};二是:合并{1,(2,3)}。可更改表示方法,比如m[i][j]:表示合并从第i堆开始的往下j堆石子,总共j+1堆。即a[i],a[i+1],a[i+j]。
首尾相连方法有两个:
1.遍历数组的时候将索引从0到2*n之间遍历,但是每次都要%n来求真实索引;
2.直接扩展数组,将两个一模一样的数组连接在一起,然后按照正常索引遍历。
算法设计
算法设计过程:
根据算法分析,本设计采用方法1使石子首尾相连并用动态规划求解。
动态规划强调的是状态和选择,所以需先来分析题目中的状态:首先状态一定有当前石子的数目,其次就是最后的得分,根据这两个状态就可以写出状态转移方程。在写出状态转移方程之前,需要寻找分割点,将多个相连的项分开。确定一个分割点,在分割点前的石子合并,在分割点后的石子合并,最后将二块一起合并,我们的状态转移方程也应根据这个分割点来写。
动态规划的求解步骤为:划分阶段—>确定状态和状态变量—>找出状态转移方程—>找出结束条件。
对于石子合并问题的问题阶段可分为:当合并的石子为一堆时候:分数为0;当合并的石子为两堆时候:合并分数为相邻两堆石子的个数之和;当合并的石子为三堆时候:合并分数为dp(第i堆石子与第i+1石子合并的分数+三堆石子总数,第i+1堆石子与第i+2石子合并的分数+三堆石子总数)......以此类推。
状态变量:dp[i][j]指第i堆至第j堆石子合并时候的分数,即每一阶段中各个不同合并方法的石子合并总得分。
状态转移方程:dp[i][j]=dp(dp[i][k]+dp[k+1][j]+sum);sum表示第i堆石子至第j堆石子总数,也是最后一次合并的分数。
通过状态转移方程我们可以看出dp[i][t]表示从第i堆开始之后合并t堆石子(包括第i堆石子)合并花费,k是i至t之间的一个数,因为是环形,所以要将一维数组首尾相连,所以计算第i+k堆应该表示成: (i+k-1)%n+1,用此方法避开0,用sum表示最后两队合并时候的花费,sum一定等于所有石子的个数。
以4堆石子为例,假设石子数为4,4,5,9,建表如下:
i\j | 1 | 2 | 3 | 4 |
1 | 0 | A11+A22+SUM12 =4+4=8 | A11+A23+SUM13=9+13=22; A12+A33+SUM13=8+13=21 | A11+A24+SUM14=32+22=54; or=27+22=49; A12+A34+SUM14=8+14+22=44; A13+A44+SUM14=22+22=44; or=21+22=43 |
2 | 0 | A22+A33+SUM23=4+5=9 | A22+A34+SUM24=14+18=32; A23+A44+SUM24=9+18=27 | |
3 | 0 | A33+A44+SUM34=5+9=14 | ||
4 | 0 |
dp[i][j]的含义为从第i堆开始,合并j堆石子能得到的最优值,则易得状态转移方程为dp[i][j]=better(dp[i][j],dp1[i][k]+dp[(i+k-1)%n+1][j-k]+sum[i][j]);该状态转移方程为运用取模运算方法强行将石子围成圈,从1到n后又马上接上1,完美地避开了0;
伪代码:
算法名称:石子合并动态规划算法
输入:石子堆数目n,每堆石子的数量S=(s1,s2,….,sn)
输出:总合并得分的最大值与最小值
最大最小得分数组dp1,dp2的初始化
for合并的堆数lenth从1到n-1依次增加 do
for合并的起点i从1到n-lenth do
合并的终点j=i+lenth
for 中间点k从i到j移动 do
求出当前这一步从i到j的代价sum[i][j]
执行状态转移方程,求出dp[i][j]的最大值与最小值
end for
end for
end for
输出dp1[1][n]和dp2[1][n]
复杂度分析:
一维动态规划时间复杂度一般有O(n)和O(n^2)两种,时间复杂度取决于状态转移方程。
如果第i个状态的确定需要利用前i-1个状态,即dp[i]由dp[i-1],dp[i-2],…,dp[0]的取值共同决定,那么此时的时间复杂度为此算法的时间复杂度为O(n2)。
实验验证
1.正确性验证:
验证1:
输入: | 3 1 2 |
输出: | 4 6 |
验证2:
输入: | 6 30 35 15 5 10 20 21 |
输出: | 275 475 |
验证3:
输入: | 4 4 4 -5 9 |
输出: | 14 42 |
验证4:
输入: | 36 53 49 2 9 9 30 2 35 1 46 39 46 42 33 13 41 35 57 38 59 15 40 18 6 46 30 53 31 34 57 41 20 1 42 59 46 |
输出: | 5913 24595 |
验证5:
输入: | 6 3 4 5 6 7 8 |
输出: | 84 125 |
验证6:
输入: | 29 3 4 7 11 13 15 18 21 17 14 7 5 8 10 19 16 13 10 7 5 4 3 4 5 6 3 15 3 10 |
输出: | 1289 5081 |
验证7:
输入: | 3 1 3 15 |
输出: | 23 37 |
验证8:
输入: | 6 1 7 6 12 3 15 |
输出: | 110 171 |
验证9:
输入: | 4 1 1 2 3333 |
输出: | 3343 10008 |
验证10:
输入: | 86 14 27 48 9 8 14 9 29 25 14 8 30 37 37 4 4 3 6 39 40 19 30 22 37 25 17 41 41 7 5 4 3 10 33 12 28 13 18 42 16 16 33 34 45 16 24 15 38 37 28 36 21 27 30 44 33 6 24 20 6 3 27 33 4 46 42 34 46 14 35 36 25 33 8 12 47 18 7 49 16 3 5 43 28 35 5 |
输出: | 12533 95356 |
验证11:
输入: | 5 8 30 37 4 4 |
输出: | 153 304 |
2.有效性验证
经过输入11组数据,得出以下几种结果:
A.验证1石子数据少一组,无法运行出正确结果;
B.验证2石子数据多一组,程序运行时只读取前n组数据,多余的数据被舍弃,程序运行得出正确结果;
C.验证3 石子数据出现负数,程序运行没有识别负数数据,得出结果正确,但不符合实际情况;
D.验证4-11 石子数数据变化大,但程序运行都得出正确结果。
经过验证,错误情况程序无法运行,在输入数据合法的情况下输出结果均有效且正确,故算法有效、正确。
总结:
题目要求只能将两个相邻的石子合并,起初可能会认为应该使用贪心法来解决,每次合并最大的两个石子,就可以算出最大值,但实际应用后会发现这是完全错误的,因为每一堆石子是有序排列的,所以这个问题不具有局部最优代替整体最优的性质,不能使用贪心法。但是可以发现,这个问题确实具有子问题,子问题就是可以选择各种不同的顺序进行石子堆合并。问题虽然不具有局部最优代替整体最优的性质,但是其具有各种子问题,且子结构有重叠部分,所以自然而然可以想到使用动态规划法来解决这个问题。
可能存在的问题:
A.输入数据有误:石子数不能为0或为负数,程序无法判断输入数据是否合法,所以需输入数据时保证输入数据符合实际情况,不能为0或为负数。
B.输入数据不足:石子堆数输入应等于第一行的石子堆数,当输入石子数据堆数少于第一行的石子堆数时程序无法运行;
附录1:源代码
#include<stdio.h>
#define M 110
const int INF = 1000000000;
int dp1[M][M],dp2[M][M]; //定义最优解数组,用于后续得出最大得分和最小得分
int sum[M][M];//存放石子堆合并后石子数量
int num[M];//定义石子堆数量数组用于存放各个石子堆石子数
//以下两个函数为比较大小函数 ,通过对他们的调用返回两数比较的结果
int min(int a,int b)
{
return a<b?a:b;//返回较小值
}
int max(int a,int b)
{
return a>b?a:b;//返回较大值
}
//主函数
int main(void){
FILE *fp1,*fp2;//定义文件指针
fp1=fopen("H:\\虞洋\\石子数合并\\测试\\input.txt","r+");//打开数据输入文件
fp2=fopen("H:\\虞洋\\石子数合并\\测试\\output.txt","w+");//打开数据输出文件
int n,i,j,k;//定义变量,n为石子堆堆数量,i,j,k为控制循环的变量
fscanf(fp1,"%d",&n);//读入石子堆堆数量
for(i=1;i<=n;i++)
fscanf(fp1,"%d",&num[i]);//将石子堆数量逐一读入数组
for(i=1;i<=n;i++)
sum[i][1]=num[i];//将石子堆数量存入矩阵第一列,为准备后续计算
//循环得出sum数组
for(j=2;j<=n;j++)
for(i=1;i<=n;i++)
sum[i][j]=sum[i%n+1][j-1]+num[i];
for(i=0;i<=n;i++)
dp1[i][1]=dp2[i][1]=0;
//动态规划过程实现
for(j=2;j<=n;j++) {
for(i=1;i<=n;i++){
dp1[i][j]=0; //赋dp1初值为0
dp2[i][j]=INF;//赋dp2初值为极大值
for(k=1;k<j;k++){
dp1[i][j]=max(dp1[i][j],dp1[i][k]+dp1[(i+k-1)%n+1][j-k]+sum[i][j]);//每一步合并后的最大值,比较得出最大得分
dp2[i][j]=min(dp2[i][j],dp2[i][k]+dp2[(i+k-1)%n+1][j-k]+sum[i][j]);//每一步合并后的最小值,比较得出最小得分
}
}
}
int ansmi=INF,ansmx=0;
for(i=1;i<=n;i++){
ansmx=max(ansmx,dp1[i][n]);//得出最大得分
ansmi=min(ansmi,dp2[i][n]);//得出最小得分
}
fprintf(fp2,"%d\n%d\n",ansmi,ansmx);//输出最小得分和最大得分
fclose(fp1);
fclose(fp2);
return 0;
}