【动态规划】小石子游戏-石子合并

博客围绕小石子游戏展开,该游戏有路边和操场两种玩法,目标是求石子合并的最小或最大成本。采用动态规划算法,利用最优子结构和递推公式求解。操场版是路边版的升级版,可转化为多个路边版求解,还给出了不同版本的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

一群小孩子在玩小石子游戏,游戏有两种玩法。
(1)路边玩法
有n堆石子堆放在路边,现要将石子有序地合并成一堆,规定每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费(最小或最大)。
(2)操场玩法
一个圆形操场周围摆放着n堆石子,现要将石子有序地合并成一堆,规定每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费(最小或最大)。

问题分析

这个游戏用不同的合并方法就会有不同的成本,我们要求得成本的最值,这两种玩法的区别在于路边版是把石子排成一条直线,而操场版是把石子排成一个圆圈,那么操场版就可以在路边版的基础上求解。

算法

这部分有参考《趣学算法》,也有很多自己的理解和思考,如有bug,欢迎批评指正。(鞠躬)

动态规划

这个问题同样具有最优子结构,也就是整个问题的最优解会包含子问题的最优解,类似于编辑距离游艇租赁问题,证明可以采用反证法,具体证明这里不再给出,与前面两类问题相似,如感兴趣可移步前两类问题。

算法核心

这个问题在对于求解子问题的部分与游艇租赁问题非常类似,也是先求出堆数少的石子合并的成本,再利用已求的子问题的最优解来求堆数较多的石子合并的成本,最后求出所有石子合并的成本,其中最重要的当然是递推公式啦。
a[]:记录每堆石子的数目;
Min[i][j]:从第i堆到第j堆合并的最小成本;
Max[i][j]:从第i堆到第j堆合并的最大成本;
k:第i堆和第j堆中间的石子堆;
r(i,j):从第i堆到第j堆的总石子数;
最小成本的递推公式:
if i== j,Min[i][j]=0;
if i!=j,Min[i][j]=min{Min[i][j],Min[i][k]+Min[k+1][j]+r(i,j)};
最小成本的递推公式:
if i== j,Min[i][j]=0;
if i!=j,Max[i][j]=max{Max[i][j],Max[i][k]+Max[k+1][j]+r(i,j)};
这个递推公式还是比较好理解的,就是在石子堆i和j之间找到一个石子堆k使得先把i到k的合并,k+1到j的合并,再把这两个石子堆合并,这样合并的成本最小。

算法流程

用a[]存储每堆的石子数目,sum[]计算到当前堆的总石子数,将Min[][]初始化为无穷大,Max[][]初始化为-1(这是求最值的技巧,最小值初始化为无穷大,最大值初始化为负,具体数值视情况而定),然后把Min[i][i]和Max[i][i]都置为0。之后按照距离大小设循环,d从1到n-1,然后距离确定的时候,从第一个堆开始每个堆都求到一定距离堆的最小和最大成本,这个就用之前的递推公式即可。举个栗子来说就是,比如距离为3,从第1个堆开始求,就是先求从第1个堆合并到第4个堆的最小和最大成本,那么k可以取1,2,3(注意不能取4,因为划分的时候第二部分是从k+1开始的,如果取4就会超出范围),之后设循环计算得到最值即可。路边版的最大和最小成本分别是Max[1][n],Min[1][n]。

操场版————路边版的升级版

之前说过路边版是把石子排成直线,而操场版是把石子排成圆圈,其实算法是一样的,只不过操场版的规模比路边版更大。路边版是n堆,操场版是2*n-1堆,就是之前n堆和路边版是一样的,之后的就是从a[1]到a[n-1],无论从a[1]到a[n]哪一堆开始向后数n个都是一个路边版,而且每一个都是操场版都是在圆圈中可以实现的,换句话说一个操场版可以看成n个路边版,那么求最值只需要在这n个路边版的最值中求即可。

代码实现

石子合并游戏(路边版&&line版)
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=105;
const int INF=1<<29;

int n;
int a[maxn];//存储每堆的石子数
int Min[maxn][maxn];//记录两堆合并的最小成本
int Max[maxn][maxn];//记录两堆合并的最大成本
int sum[maxn];//记录到当前堆数的总石子数,以便计算最后两堆合并时的成本

void init()
{
    int cnt=0;
    int i,j;
    memset(sum,0,sizeof(sum));
    for(i=1; i<=n; ++i)
    {
        cnt+=a[i];
        sum[i]=cnt;
    }
    for(i=1; i<=n; ++i)
        for(j=i; j<=n; ++j)
        {
            Min[i][j]=INF;//将最小成本初始化为无穷大
            Max[i][j]=-1;//将最大成本初始化为-1
        }
}

int r(int x,int y)//计算从第x堆到第y堆合并过程中最后一次合并所需成本,也就是从x到y的总石子数
{
    return sum[y]-sum[x-1];
}

void strstone()
{
    int i,j,k,d;
    for(i=1; i<=n; ++i) //初始化,由自己到自己成本为0
    {
        Min[i][i]=0;
        Max[i][i]=0;
    }
    for(d=1; d<=n-1; ++d) //按照距离计算
    {
        for(i=1; i<=n-d; ++i) //从第1堆开始
        {
             j=i+d;
            for(k=i; k<i+d; ++k)
            {
                Min[i][j]=min(Min[i][j],Min[i][k]+Min[k+1][j]+r(i,j));
                Max[i][j]=max(Max[i][j],Max[i][k]+Max[k+1][j]+r(i,j));
            }
        }
    }
}

int main()
{
    cout<<"请输入石子堆数:";
    cin>>n;
    cout<<"请依次输入每堆的石子数:";
    int i;
    for(i=1; i<=n; ++i)
        cin>>a[i];
    init();
    strstone();
    cout<<"石子游戏(路边版)的最小成本为:"<<Min[1][n]<<endl;
    cout<<"石子游戏(路边版)的最大成本为:"<<Max[1][n]<<endl;
    return 0;
}
石子合并游戏(操场版&&circle版)
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=105*2;
const int INF=1<<29;

int n,m;//实际石子堆数,计算石子堆数
int a[maxn];//存储每堆的石子数
int Min[maxn][maxn];//记录两堆合并的最小成本
int Max[maxn][maxn];//记录两堆合并的最大成本
int sum[maxn];//记录到当前堆数的总石子数,以便计算最后两堆合并时的成本
int min_num,max_num;//最小和最大成本

void init()
{
    int cnt=0;
    int i,j;
    memset(sum,0,sizeof(sum));
    for(i=1; i<=m; ++i)
    {
        cnt+=a[i];
        sum[i]=cnt;
    }
    for(i=1; i<=m; ++i)
        for(j=i; j<=m; ++j)
        {
            Min[i][j]=INF;//将最小成本初始化为无穷大
            Max[i][j]=-1;//将最大成本初始化为-1
        }
}

int r(int x,int y)//计算从第x堆到第y堆合并过程中最后一次合并所需成本,也就是从x到y的总石子数
{
    return sum[y]-sum[x-1];
}

void playstone()
{
    int i,j,k,d;
    for(i=1; i<=m; ++i) //初始化,由自己到自己成本为0
    {
        Min[i][i]=0;
        Max[i][i]=0;
    }
    for(d=1; d<=m-1; ++d) //按照距离计算
    {
        for(i=1; i<=m-d; ++i) //从第1堆开始
        {
             j=i+d;
            for(k=i; k<i+d; ++k)
            {
                Min[i][j]=min(Min[i][j],Min[i][k]+Min[k+1][j]+r(i,j));
                Max[i][j]=max(Max[i][j],Max[i][k]+Max[k+1][j]+r(i,j));
            }
        }
    }
}

void cal()
{
          min_num=INF;
          max_num=-1;
          for(int i=1;i<=n;++i)
          {
                    if(Min[i][i+n-1]<min_num)
                              min_num=Min[i][i+n-1];
                    if(Max[i][i+n-1]>max_num)
                              max_num=Max[i][i+n-1];
          }
}

int main()
{
    cout<<"请输入石子堆数:";
    cin>>n;
    m=2*n-1;
    cout<<"请依次输入每堆的石子数:";
    int i;
    for(i=1; i<=n; ++i)
        cin>>a[i];
        for(i=n+1;i<=m;++i)
          a[i]=a[i-n];
    init();//初始化
    playstone();
    cal();//计算最小成本和最大成本
    cout<<"石子游戏(操场版)的最小成本为:"<<min_num<<endl;
    cout<<"石子游戏(操场版)的最大成本为:"<<max_num<<endl;
    return 0;
}
石子合并游戏(综合版&&classic版)

这个版本是二者的结合,路边版的数据是可以直接从操场版中的得到的,其实操场版中就包含了路边版,所以这个版本与操场版的区别就在于主函数。

#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int maxn=105*2;
const int INF=1<<29;

int n,m;//实际石子堆数,计算石子堆数
int a[maxn];//存储每堆的石子数
int Min[maxn][maxn];//记录两堆合并的最小成本
int Max[maxn][maxn];//记录两堆合并的最大成本
int sum[maxn];//记录到当前堆数的总石子数,以便计算最后两堆合并时的成本
int min_num,max_num;//最小和最大成本

void init()
{
    int cnt=0;
    int i,j;
    memset(sum,0,sizeof(sum));
    for(i=1; i<=m; ++i)
    {
        cnt+=a[i];
        sum[i]=cnt;
    }
    for(i=1; i<=m; ++i)
        for(j=i; j<=m; ++j)
        {
            Min[i][j]=INF;//将最小成本初始化为无穷大
            Max[i][j]=-1;//将最大成本初始化为-1
        }
}

int r(int x,int y)//计算从第x堆到第y堆合并过程中最后一次合并所需成本,也就是从x到y的总石子数
{
    return sum[y]-sum[x-1];
}

void playstone()
{
    int i,j,k,d;
    for(i=1; i<=m; ++i) //初始化,由自己到自己成本为0
    {
        Min[i][i]=0;
        Max[i][i]=0;
    }
    for(d=1; d<=m-1; ++d) //按照距离计算
    {
        for(i=1; i<=m-d; ++i) //从第1堆开始
        {
            j=i+d;
            for(k=i; k<i+d; ++k)
            {
                Min[i][j]=min(Min[i][j],Min[i][k]+Min[k+1][j]+r(i,j));
                Max[i][j]=max(Max[i][j],Max[i][k]+Max[k+1][j]+r(i,j));
            }
        }
    }
}

void cal()
{
          min_num=INF;
          max_num=-1;
          for(int i=1;i<=n;++i)
          {
                    if(Min[i][i+n-1]<min_num)
                              min_num=Min[i][i+n-1];
                    if(Max[i][i+n-1]>max_num)
                              max_num=Max[i][i+n-1];
          }
}

int main()
{
    cout<<"请输入石子堆数:";
    cin>>n;
    m=2*n-1;
    cout<<"请依次输入每堆的石子数:";
    int i;
    for(i=1; i<=n; ++i)
        cin>>a[i];
        for(i=n+1;i<=m;++i)
          a[i]=a[i-n];
    init();//初始化
    playstone();
    cout<<"石子合并游戏(路边版)的最小成本为:"<<Min[1][n]<<endl;
    cout<<"石子合并游戏(路边版)的最大成本为:"<<Max[1][n]<<endl;
    cal();//计算操场版最小成本和最大成本
    cout<<"石子游戏(操场版)的最小成本为:"<<min_num<<endl;
    cout<<"石子游戏(操场版)的最大成本为:"<<max_num<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值