石子合并问题

问题描述:

在一个圆形操场的四周摆放着 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;

}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你代码有bug!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值