合并石子问题研究与实现

一、简单情况

(1)问题描述

  有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。求将这N堆石子合并成一堆的总花费最小(或最大)。

例题:

      Description

     佳佳的爷爷有一个苹果园,最近苹果都熟了,都从树上掉了下来,佳佳要帮爷爷把树下的苹果堆到一起,他每次可以任意选两堆合成一堆,耗费的体力是这两堆苹果苹果数量的和。他最后的目标是将所有的果子堆成一个大堆,那么他最少耗费的体力是多少呢?

      Input

     多组测试数据,每组数据输入的第一行是一个数n,表示果子原来有多少堆,接下来n个数,每个数表示每堆苹果的个数。

      Output

      对于每组数据,输出一个整数,表明佳佳最少需要的体力。

      Sample Input

      3
      1 1 1

      Sample Output

     5

(2)分析

由于每次可以选择任意的两堆,所以可以选择贪心算法,一种简单的思路是使用sort方法排序之后每次取数组的前两位相加,合并后的新堆需要按大小放到后面剩余的位置上即可。代码如下:

#include <iostream>
#include <algorithm>
#define N 300
using namespace std;
const int INF=1<<10;
int main(){
	int n,i,j,k;
	while(cin>>n){
		int resmin=0;
		int a[n];
		for(i=0;i<n;i++){
			cin>>a[i];
		}
		sort(a,a+n);
		for(i=0;i+1<n;i++){
			resmin+=a[i]+a[i+1];
			a[i+1]=a[i]+a[i+1];
			k=i+1;
			for(j=i+2;j<n;j++){
				if(a[k]>a[j]){
					int temp=a[j];
					a[j]=a[k];
					a[k]=temp;
					k=j;
				}
			}
		}
			
	cout<<resmin<<endl;
	}
	return 0;
	
}

但是在OJ上会TimeLimitExceeded,算法本身结果没问题,但是超时了。那么可以换一种思路,该数组的性质符合stl的queue中的优先队列,直接用优先队列构造这组数据即可更高效的实现,代码如下:

#include <iostream>
#include <queue>
using namespace std;
int main()
{
    int n;
    while (cin>>n)
    {
          priority_queue<int,vector<int>,greater<int> > que;   //greater< >指定是小顶堆 ,即从小到大排列 
          int i, crt, cur, a, m, resmin = 0;
          for (i = 0; i < n; ++i)
          {
              scanf("%d", &a);
              que.push(a);
          }
          while (que.size() > 1)
          {
                crt = que.top();
                que.pop();
                cur = que.top();
                que.pop();
                m =cur + crt;
                resmin += cur + crt;
                que.push(m);
          }
          cout<<resmin<<endl;
    }
    system("pause");
    return 0;
}

以上代码可以AC。到此最简单的情况基本解决了,下面讨论复杂一些的情况。

二、线型情况

(1)问题描述

      在一个操场上摆放着一行共n堆的石子。现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆石子数记为该次合并的得分。请编辑计算出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。

Input

      输入第一行为n(n<1000),表示有n堆石子,第二行为n个用空格隔开的整数,依次表示这n堆石子的石子数量(<=1000)

Output

      输出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。

 Sample Input

      3

      1 2 3

 Sample Output

      9 11

(2)分析

      每次只能选择相邻的两堆合并,所以继续使用贪心算法会有问题,原理是局部最优与全局最优并不是完全一致的。考虑使用动态规划的方法。动态规划需要建立状态转移方程,首先考虑最后一步时的状态,即将两大堆合为一堆,那么只要保证这两堆已为最优解即可。此时有两种思路:

思路一:建立二维素组dp[ ][ ],dp[i][j]代表从第i个石堆合并到第j个石堆的最优解,0<=i<=j<n;建立数组sum[ ],sum[i]代表从第0个石堆加到第i个石堆的总和,因为要合并一个区间的石堆,无论顺序如何最终一次合并的得分都为所有石堆的总和;

状态转移方程:以求最小得分为例:

即将i~j堆石头的最优解分解为i~k段加上k+1~j段最优解的和加上最后将这两段合并的代价temp:

c++实现代码:

#include <iostream>
using namespace std;
const int INF = 1 << 30;
#define N  1000

int dpmin[N][N],dpmax[N][N];
int sum[N];
int a[N],resmin,resmax;


int main()
{
    int n;
    while(cin>>n)
    {
        for(int i=0;i<n;i++)
            cin>>a[i];
        sum[0] = a[0];
        for(int i=1;i<n;i++)
            sum[i] = sum[i-1] + a[i];
        
    for(int i=0;i<n;i++)
        dpmin[i][i] =dpmax[i][i] =0; //根据从i堆到i堆相当于不合并,所以为0 
    for(int v=1;v<n;v++) //步长从1到n-1
    {
        for(int i=0;i<n-v;i++)  //从第0堆石头开始循环,i<n-v因为是线型的不能i+步长v不能超出n-1    
        {
            int j = i + v;     //求i~j的最优解 
            dpmin[i][j] = INF;//置i!=j的位置为一较大值 
            dpmax[i][j]=0;    //置i!=j的位置为一较小值0 
            int tmp = sum[j] - (i > 0 ? sum[i-1]:0);
            for(int k=i;k<j;k++){
            	dpmin[i][j] = min(dpmin[i][j],dpmin[i][k]+dpmin[k+1][j] + tmp);
                dpmax[i][j] = max(dpmax[i][j],dpmax[i][k]+dpmax[k+1][j] + tmp);
			}

        }
    }
	resmin=dpmin[0][n-1];
	resmax=dpmax[0][n-1];
	cout<<resmin<<endl<<resmax<<endl;
	
    }
    return 0;
}

思路二:建立二维素组dp[ ][ ],dp[i][j]代表从第i个石堆开始合并j个石堆(总共j+1个石堆)的最优解,0<=i<n-j,1<=j<n;建立数组sum[ ]含义与上面相同;

状态转移方程:以求最小得分为例,

即将从i堆开始的j堆石头的合并分解为从i堆开始的k堆的最优解加上从i+k+1堆开始的j-k-1堆石头的最优解再加上将这两段合并的代价temp:

c++实现代码:

#include <iostream>
using namespace std;
const int INF=1<<10;
#define N 200
int a[N],sum[N];
int dpmax[N][N],dpmin[N][N];
int resmin,resmax;
int main(){
	int n,i,j,k,t;
	while(cin>>n){
		for(i=0;i<n;i++){
			cin>>a[i];
		}
	sum[0]=a[0];
	for(i=1;i<n;i++){
		sum[i]=sum[i-1]+a[i];
	}
	
	for(i=0;i<n;i++){  //置从i开始的0推石头为0 
		dpmax[i][0]=0;
		dpmin[i][0]=0;
	}
	for(j=1;j<n;j++){ //从某一位置开始的1堆循环到某一位置开始的n-1堆 
		for(i=0;i<n-j;i++){ //从第0堆石头开始循环,i<n-v因为是线型的不能i+步长v不能超出n-1    
			dpmin[i][j] = INF;//置j!=0的位置为一较大值 
            dpmax[i][j] = 0;  //置j!=0的位置为一较小值0 
            for(k=0;k<j;k++){
            	int temp=sum[i+j]-(i>0? sum[i-1]:0);
            	dpmin[i][j]=min(dpmin[i][j],dpmin[i][k]+dpmin[i+k+1][j-k-1]+temp);
            	dpmax[i][j]=max(dpmax[i][j],dpmax[i][k]+dpmax[i+k+1][j-k-1]+temp);
			}
		}
	}
	resmin=dpmin[0][n-1];
	resmax=dpmax[0][n-1];
	
	
	cout<<resmin<<endl<<resmax<<endl;
	}
	return 0;
	
}

三、环型情况

(1)问题描述

在一个园形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分.

    Input

数据的第1行试正整数N,1≤N≤100,表示有N堆石子.第2行有N个数,分别表示每堆石子的个数.

    Output

输出共2行,第1行为最小得分,第2行为最大得分。

Sample Input

   4
4 4 5 9

Sample Output

43
     54

(2)分析

此种情况与第二种情况大体相同,区别是石子是环形摆放的,那么在状态方程迭代时要进行一些对n取模处理。

思路一:  思路与情况二大体一致,不再赘述。相当于对n排石子求了最优解,第一排0~n-1堆,第二排1~n-1加上0~1,第三排2~n-1加上0~2,···
 

c++代码:

#include <iostream>
#include <string.h>
#include <stdio.h>

using namespace std;
const int INF = 1 << 30;
const int N = 200;

int dpmin[N][N];
int dpmax[N][N];
int sum[N],a[N];
int resmin,resmax;
int n,i,j,k,v;

int getsum_ring(int i,int j)  //因为是环形使求temp变得复杂,需要对区间i和j进行判断
{
    if(j < i) return getsum_ring(i,n-1) + getsum_ring(0,j);//如果j<i,那么相当于j超出了n-1到了环的小端,相当于(sum[n-1]-sump[i-1])+(sum[j]-0) 
    else return sum[j] - (i>0 ? sum[i-1]:0);
}

void dp(int a[],int n)
{
    for(i=0;i<n;i++)
        dpmin[i][i] = dpmax[i][i] = 0;//根据从i堆到i堆相当于不合并,所以为0 
    for(v=1;v<n;v++) //步长从1到n-1
    {
        for(i=0;i<n;i++)
        {
        	j=(i+v)%n;  //因为是一个环,所以i+v可以超过n-1,进行对n取模操作,n等价于0,n+1等价于1 
            dpmin[i][j] = INF;//置i!=j的位置为一较大值 
            dpmax[i][j] = 0;  //置i!=j的位置为一较小值0 
			for(k=0;k<v;k++){
				dpmin[i][j] = min(dpmin[i][j],dpmin[i][(i+k)%n] + dpmin[(i+k+1)%n][j] + getsum_ring(i,j)); //i+k,i+k+1都有可能超过1,所以都对n取模 
                dpmax[i][j] = max(dpmax[i][j],dpmax[i][(i+k)%n] + dpmax[(i+k+1)%n][j] + getsum_ring(i,j));
			}
        }
    }
    resmin = dpmin[0][n-1];
    resmax = dpmax[0][n-1];
    for(i=0;i<n;i++)   //相当于对n排石子求了最优解,第一排0~n-1堆,第二排1~n-1加上0~1,第三排2~n-1加上0~2,···现在在这n种情况中再找出最优解 
    {
        resmin = min(resmin,dpmin[i][(i+n-1)%n]);
        resmax = max(resmax,dpmax[i][(i+n-1)%n]);
    }
}

int main()
{
    while(cin>>n)
    {
        for(i=0;i<n;i++)
        	cin>>a[i];
        sum[0] = a[0];
        for(i=1;i<n;i++)
            sum[i] = sum[i-1] + a[i];
        dp(a,n);
        cout<<resmin<<endl;
        cout<<resmax<<endl;
    }
    return 0;
}

思路二: 思路与情况二大体一致,不再赘述。一些细节问题需要重新处理,例如temp的计算

c++代码:

#include <iostream>
using namespace std;
const int INF = 1 << 30;
const int N = 200;

int dpmin[N][N];
int dpmax[N][N];
int sum[N],a[N];
int resmin,resmax;
int n;

int getsum_ring(int i,int j) //因为是环形使求temp变得复杂,需要对区间i和j进行判断
{
    if(i+j >= n) return getsum_ring(i,n-i-1) + getsum_ring(0,(i+j)%n);//如果i+j>=n ,那么此时temp应为i堆之后的n-i-1堆(等价于sum[n-1]-sum[i-1])之和加上0~(i+j)%n堆之和 
    else return sum[i+j] - (i>0 ? sum[i-1]:0);
}

void dp(int a[],int n)
{
    for(int i=0;i<n;i++)    //置从i开始的0推石头为0 
        dpmin[i][0] = dpmax[i][0] = 0;
    for(int j=1;j<n;j++)  //从某一位置开始的1堆循环到某一位置开始的n-1堆 
    {
        for(int i=0;i<n;i++) //从第0堆石头开始循环,i<n因为是环形的可以超出n-1    
        {
            dpmin[i][j] = INF;//置j!=0的位置为一较大值 
            dpmax[i][j] = 0;  //置j!=0的位置为一较小值0 
            for(int k=0;k<j;k++)
            {
                dpmin[i][j] = min(dpmin[i][j],dpmin[i][k] + dpmin[(i+k+1)%n][j-k-1] + getsum_ring(i,j)); // dpmin[i][k]中的k代表从i堆开始的k堆,不代表堆得序号所以不用对N取模,
                dpmax[i][j] = max(dpmax[i][j],dpmax[i][k] + dpmax[(i+k+1)%n][j-k-1] + getsum_ring(i,j));//dpmin[(i+k+1)%n][j-k-1]中的(i+k+1)代表堆序号且可能超过n-1所以需要取模 
            }
        }
    }
    resmin = dpmin[0][n-1];
    resmax = dpmax[0][n-1];
    for(int i=0;i<n;i++)  //相当于对n排石子求了最优解,第一排0~n-1堆,第二排1~n-1加上0~1,第三排2~n-1加上0~2,···现在在这n种情况中再找出最优解 
    {
        resmin = min(resmin,dpmin[i][n-1]);
        resmax = max(resmax,dpmax[i][n-1]);
    }
}

int main()
{
    while(cin>>n)
    {
        for(int i=0;i<n;i++)
        	cin>>a[i];
        sum[0] = a[0];
        for(int i=1;i<n;i++)
            sum[i] = sum[i-1] + a[i];
        dp(a,n);
        cout<<resmin<<endl;
        cout<<resmax<<endl;
    }
    return 0;
}

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值