问题描述:在一个圆形操场的四周摆放着n堆石子。现要将石子有次序地合并成一堆。规定每次只能选择相邻的两堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。
算法设计:试设计一个算法,计算出将n堆石子合并成一堆的最小得分和最大得分。
样例输入:由input.txt提供输入数据
样例输出:输出数据保存到output.txt
输入文件示例:input.txt
4
4 4 5 9
输出文件示例:output.txt
43
54
二、问题分析(以求最大得分为例)
(1).先看石子排成一排的问题
石子数量如下:
4 4 5 9
求其最大的合并值
m[i][j]=m[i][k]+m[k+1][j]+getsum(i,j);
这里的i,j是指石子的下标,第i堆,第j堆。m[i][j]表示合并第i堆到第j堆的石子的最大得分。其中getsum=a[i]+...+a[j];表示加上合并后总的石子数量。
(2).如果石子是按环形排列
环形排列存在的问题:
合并方向不明确的问题,比如m[1][3]:
在线性问题中,它表示的是讲a[1],a[2],a[3]三堆石子合并起来,然而在环形排列中我们它存在两种情况。一是:合并a[1],a[2],a[3];二是:合并a[3],a[4],a[1]。
解决方案,更改表示方法,比如m[i][j]:
它表示的是合并从第i堆开始的往下j堆石子,总共j+1堆。即a[i],a[i+1],a[i+j]
此时又会出现一个问题:下标越界问题
解决方案:对下标取余数。
因此计算公式为
int t=m[i][k]+m[(i+k)%n+1][j-k-1]+getsum(i,j);
其中getsum的实现代码为:
int getsum(int i,int j,int n){
if(i+j<n) return su[i+j]-su[i-1];
else return (su[n]-su[i-1])+(su[j-(n-i)]);
}
这里的s[i]表示第1堆加到第i堆的石子总数。
(3).数据储存问题
m[i][j]储存合并第i堆到第i+j堆石子的最大得分
特殊情况:m[i][0]=0,当只有一堆石子的时候不需要合并,因此得分为0;
(4).回溯路径
在计算得分的时候,用了一个s来储存路径。
s[i][j]=k;表示第i堆到第i+j堆石子最大得分的划分方法为先合并第i堆到第i+k堆,在合并第i+k+1堆到第j堆。
代码如下:
void Traceback(int **s,int i,int j,int n,ofstream &outfile){
if(j==0) return;
Traceback(s,i,s[i][j],n,outfile);
Traceback(s,s[i][j]%n+1,j-s[i][j]-1,n,outfile);
outfile<<"Add A"<<i<<","<<(i+s[i][j]-1)%n+1;
outfile<<" and A"<<(i+s[i][j])%n+1<<","<<(i+j-1)%n+1<<endl;
}
(5).问题细分情况说明
为何在m[i][n-1]的值会不一样?1 2 3 4为例
就是因为它们各自代表了的问题的一种情况;
比如m[1][3],它的划分情况有:m[1][2]+m[4][0],m[1][1]+m[3][1],m[1][0]+m[2][2]
在这种划分情况下,只有1和4有可能最后才被合并。
m[2][3],它的划分情况有:m[2][2]+m[1][0],m[2][1]+m[4][1],m[2][0]+m[3][2]
而在这种划分情况下,只有2和1有可能最后才被合并。
从选定的位置开始划分
三、详细设计(从算法到程序)
#include<iostream>
#include<fstream>
#include<sstream>
using namespace std;
int su[256];
int getsum(int i,int j,int n){
if(i+j<n) return su[i+j]-su[i-1];
else return (su[n]-su[i-1])+(su[j-(n-i)]);
}
int DeMin(int *a,int n,int **m,int **s)
{
for(int i=1;i<=n;i++) m[i][0]=0;
for(int j=1;j<=n-1;j++)
for(int i=1;i<=n;i++){//i,j互换位置,按照j=0,j=1的顺序对矩阵进行填值
int k=j-1;
m[i][j]=m[i][k]+m[(i+k)%n+1][0]+getsum(i,j,n);
s[i][j]=k;
for(k=j-2;k>=0;k--){
int t=m[i][k]+m[(i+k)%n+1][j-k-1]+getsum(i,j,n);
if(t<m[i][j]){m[i][j]=t;s[i][j]=k;}
}
}
}
int DeMax(int *a,int n,int **m,int **s)
{
for(int i=1;i<=n;i++) m[i][0]=0;//只有一堆石子
for(int j=1;j<=n-1;j++)
for(int i=1;i<=n;i++){//i,j互换位置,按照j=0,j=1的顺序对矩阵进行填值
int k=j-1;
m[i][j]=m[i][k]+m[(i+k)%n+1][0]+getsum(i,j,n);
s[i][j]=k;
for(k=j-2;k>=0;k--){
int t=m[i][k]+m[(i+k)%n+1][j-k-1]+getsum(i,k,n);
if(t>m[i][j]){m[i][j]=t;s[i][j]=k;}
}
}
}
void Traceback(int **s,int i,int j,int n,ofstream &outfile){
if(j==0) return;
Traceback(s,i,s[i][j],n,outfile);
Traceback(s,s[i][j]%n+1,j-s[i][j]-1,n,outfile);
outfile<<"Add A"<<i<<","<<(i+s[i][j]-1)%n+1;
outfile<<" and A"<<(i+s[i][j])%n+1<<","<<(i+j-1)%n+1<<endl;
}
int main(){
ifstream cinfile;
cinfile.open("input.txt",ios::in);
int n;
cinfile>>n;
int Stonum[n+1];
Stonum[0]=0;
for(int i=1;i<=n;i++) cinfile>>Stonum[i];
cinfile.close();
int **SN=new int*[n+1]();
int **TN=new int*[n+1]();
for(int i=0;i<=n;i++){
SN[i]=new int[n+1];
TN[i]=new int[n+1];
}
for(int i=0;i<=n;i++)
for(int j=0;j<=n;j++){
SN[i][j]=0;
TN[i][j]=0;
}
su[0]=0;
for(int i=1;i<=n;i++) su[i]=su[i-1]+Stonum[i];
ofstream outfile;
outfile.open("output.txt",ios::out);
DeMin(Stonum,n,SN,TN);
int Min=SN[1][n-1],posimin=1;
for(int i=2;i<=n;i++){
if(Min>SN[i][n-1]){
Min=SN[i][n-1];
posimin=i;
}
}
outfile<<Min<<endl;
outfile<<"具体合并方案:"<<endl;
Traceback(TN,posimin,n-1,n,outfile);
DeMax(Stonum,n,SN,TN);
int Max=SN[1][n-1],posimax=1;
for(int i=2;i<=n;i++){
if(Max<SN[i][n-1]){
Max=SN[i][n-1];
posimax=i;
}
}
outfile<<Max<<endl;
outfile<<"具体合并方案:"<<endl;
Traceback(TN,posimax,n-1,n,outfile);
}
四、程序运行结果
五、分析与总结
在计算m[i][j]时应该按列进行。
for(int j=2;j<=n-1;j++)
for(int i=1;i<=n;i++){//i,j互换位置,按照j=0,j=1的顺序对矩阵进行填值
}