题目
一群小孩子在玩小石子游戏,游戏有两种玩法。
(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;
}