动态规划学习之石子归并

来源:互联网 发布:门禁卡破解软件 编辑:程序博客网 时间:2024/04/19 21:58

一.题目描述:
在一个圆形操场的四周摆放着N 堆石子(N<=100),现要将石子有次序地合并成一堆.规定每次只能选取
相邻的两堆合并成新的一堆,并将新的一堆的石子数记为该次合并的得分.编写一程序,读入堆栈数N 及每堆
栈的石子数(<=20).
(1)选择一种合并石子的方案,使用权得做N-1次合并,得分的总和最小;
(2)选择一种合并石子的方案,使用权得做N-1次合并,得分的总和最大;
输入数据:
第一行为石子堆数N;
第二行为每堆的石子数,每两个数之间用一个空格分隔.
输出数据:
从第一至第N 行为得分最小的合并方案.第N+1行是空行.从第N+2行到第2N+1行是得分最大合并方案.每
种合并方案用N 行表示,其中第i 行(1<=i<=N)表示第i 次合并前各堆的石子数(依顺时针次序输出,哪一堆先
输出均可).要求将待合并的两堆石子数以相应的负数表示.
输入输出范例:
输入:
4
4 5 9 4
输出:
-4 5 9 -4
-8 -5 9
-13 -9
22
4 -5 -9 4
4 -14 -4
-4 -18
22
二.算法分析:
竞赛中多数选手都不约而同地采用了尽可能逼近目标的贪心法来逐次合并:从最上面的一堆开始,沿顺
时针方向排成一个序列.第一次选得分最小(最大)的相邻两堆合并,形成新的一堆;接下来,在N-1堆中选得分
最小(最大)的相邻两堆合并……,依次类推,直至所有石子经N-1次合并后形成一堆.例如有6堆石子,每堆石子
数依次为3 4 6 5 4 2.要求选择一种合并石子的方案,使得做5次合并,得分的总和最小.
按照贪心法,合并的过程如下:
每次合并得分:
第一次合并3 4 6 5 4 2 5
第二次合并5 4 6 5 4 9
第三次合并9 6 5 4 9
第四次合并9 6 9 15
第五次合并15 9 24
24
总得分=5+9+9+15+24=62
但是当我们仔细琢磨后,可得出另一个合并石子的方案:
每次合并得分:
第一次合并3 4 6 5 4 2 7
第二次合并7 6 5 4 2 13
第三次合并13 5 4 2 6
第四次合并13 5 6 11
第五次合并13 11 24
24
总得分=7+13+6+11+24=61
显然,后者比贪心法得出的合并方案更优.题目中的示例故意造成一个贪心法解题的假象,诱使读者进入“陷
阱”.为了帮助读者从这个“陷阱”里走出来,我们先来明确一个问题:
1.最佳合并过程符合最佳原理
使用贪心法之所以可能出错,是因为每一次选择得分最小(最大)的相邻两堆合并,不一定保证余下的合
并过程能导致最优解.聪明的读者马上会想到一种理想的假设:如果N-1次合并的全局最优解包含了每一次
合并的子问题的最优解,那么经这样的N-1次合并后的得分总和必然是最优的.
例如上例中第五次合并石子数分别为13和11的相邻两堆.这两堆石头分别由最初的第1,2,3堆(石头数
分别为3,4,6)和第4,5,6堆(石头数分别为5,4,2)经4次合并后形成的.于是问题又归结为如何使得这两个子序
列的N-2次合并的得分总和最优.为了实现这一目标,我们将第1个序列又一分为二:第1、2堆构成子序列1,第
3堆为子序列2.第一次合并子序列1中的两堆,得分7;第二次再将之与子序列2的一堆合并,得分13.显然对于
第1个子序列来说,这样的合并方案是最优的.同样,我们将第2个子序列也一分为二:第4堆为子序列1,第5,6堆
构成子序列2.第三次合并子序列2中的2堆,得分6;第四次再将之与子序列1中的一堆合并,得分13.显然对于
第二个子序列来说,这样的合并方案也是最优的.由此得出一个结论: 6堆石子经过这样的5次合并后,得分的
总和最小.
我们把每一次合并划分为阶段,当前阶段中计算出的得分和作为状态,如何在前一次合并的基础上定义
一个能使目前得分总和最大的合并方案作为一次决策.很显然,某阶段的状态给定后,则以后各阶段的决策不
受这阶段以前各段状态的影响.这种无后效性的性质符最佳原理,因此可以用动态规划的算法求解.
2.动态规划的方向和初值的设定
采用动态规划求解的关键是确定所有石子堆子序列的最佳合并方案.这些石子堆子序列包括:
{第1堆、第2堆}、{第2堆、第3堆}、……、{第N 堆、第1堆};
{第1堆、第2堆、第3堆}、{第2堆、第3堆、第4堆}、……、{第N 堆、第1堆、第2堆}
……
{第1堆、……、第N 堆}、{第2堆、……、第N 堆、第1堆}、……、{第N 堆、第1堆、……、第N-1堆}
为了便于运算,我们用(i,j)表示一个从第i 堆数起,顺时针数j 堆时的子序列{第i 堆、第i+1堆、……、第
(i+j-1)mod n 堆}
它的最佳合并方案包括两个信息:
①在该子序列的各堆石子合并成一堆的过程中,各次合并得分的总和;
②形成最佳得分和的子序列1和子序列2.由于两个子序列是相邻的,因此只需记住子序列1的堆数;
设f(i,j)---将子序列(i,j)中的j 堆石子合并成一堆的最佳得分和;
c(i,j)---将(i,j)一分为二,其中子序列1的堆数; (1≤i≤N,1≤j≤N)
显然,对每一堆石子来说,它的f(i,1)=0, c(i,1)=0 (1≤i≤N)
对于子序列(i,j) 来说, 若求最小得分总和,f(i,j) 的初始值为∞; 若求最大得分总和,f(i,j) 的初始值为0.
(1≤i≤N,2≤j≤N).
规划的方向是顺推.
先考虑含二堆石子的N 个子序列(各子序列分别从第1堆、第2堆、……、第N 堆数起,顺时针数2堆)的合并
方案:
f(1,2), f(2,2), ……, f(N,2)
c(1,2), c(2,2),……, c(N,2)
然后考虑含三堆石子的N 个子序列(各子序列分别从第1堆、第2堆、……、第N 堆数起,顺时针数3堆)的合
并方案:
f(1,3), f(2,3), ……, f(N,3)
c(1,3), c(2,3),……, c(N,3)
……
依次类推,直至考虑了含N 堆石子的N 个子序列(各子序列分别从第1堆、第2堆、……、第N 堆数起,顺时
针数N 堆)的合并方案:
f(1,N), f(2,N), ……, f(N,N)
c(1,N), c(2,N),……, c(N,N)
最后,在子序列(1,N),(2,N),……,(N,N)中选择得分总和(f 值)最小(或最大)的一个子序列(i,N)(1≤i≤N),由此出发
倒推合并过程.
3.动态规划方程和倒推合并过程
对子序列(i,j)最后一次合并,其得分为第i 堆数起,顺时针数j 堆的石子总数t.被合并的两堆石子是由子序
列(i,k)和((i+k-1)modn+1,j-k) (1≤k≤j-1)经有限次合并形成的.为了求出最佳合并方案中的k 值,我们定义一个
动态规划方程:
当求最大得分总和时
f(i,j)=max{f(i,k)+f(x,j-k)+t} 1≤k≤j-1
c(i,j)=k │f(i,j)=f(i,k)+f(x,j-k)+t 2≤j≤n,1≤i≤n
当求最小得分总和时
f(i,j)=min{f(i,k)+f(x,j-k)+t} 1≤k≤j-1
c(i,j)=k │f(i,j)=f(i,k)+f(x,j-k)+t 2≤j≤n,1≤i≤n
其中x=(i+k-1)modn+1,即第i 堆数起,顺时针数k+1堆的堆序号.
例如对上述提到过的6堆石子,按动态规划方程顺推最小得分和.依次得出
含二堆石子的6个子序列的合并方案:
f(1,2)=7 f(2,2)=10 f(3,2)=11 f(4,2)=9 f(5,2)=6 f(6,2)=5
c(1,2)=1 c(2,2)=1 c(3,2)=1 c(4,2)=1 c(5,2)=1 c(6,2)=1
含三堆石子的6个子序列的合并方案:
f(1,3)=20 f(2,3)=25 f(3,3)=24 f(4,3)=17 f(5,3)=14 f(6,3)=14
c(1,3)=2 c(2,3)=2 c(3,3)=1 c(4,3)=1 c(5,3)=1 c(6,3)=2
含四堆石子的6个子序列的合并方案:
f(1,4)=36 f(2,4)=38 f(3,4)=34 f(4,4)=28 f(5,4)=26 f(6,4)=29
c(1,4)=2 c(2,4)=2 c(3,4)=1 c(4,4)=1 c(5,4)=2 c(6,4)=3
含五堆石子的6个子序列的合并方案:
f(1,5)=51 f(2,5)=48 f(3,5)=45 f(4,5)=41 f(5,5)=43 f(6,5)=45
c(1,5)=3 c(2,5)=2 c(3,5)=2 c(4,5)=2 c(5,5)=3 c(6,5)=3
含六堆石子的6个子序列的合并方案:
f(1,6)=61 f(2,6)=62 f(3,6)=61 f(4,6)=61 f(5,6)=61 f(6,6)=62
c(1,6)=3 c(2,6)=2 c(3,6)=2 c(4,6)=3 c(5,6)=4 c(6,6)=3
f(1,6)是f(1,6),f(2,6),……,f(6,6)中的最小值,表明最小得分和是由序列(1,6)经5次合并得出的.我们从这个序
列出发,按下述方法倒推合并过程:
由c(1,6)=3可知,第5次合并的两堆石子分别由子序列(1,3)和子序列(4,3)经4次合并后得出.其中c(1,3)=2可
知由子序列(1,3)合并成的一堆石子是由子序列(1,2)和第三堆合并而来的.而c(1,2)=1,已表明了子序列(1,2)
的合并方案是第1堆合并第2堆.由此倒推回去,得出第1第2次合并的方案:
每次合并得分
第一次合并3 4 6……7
第二次合并7 6……13
13……
子序列(1,3)经2次合并后合并成1堆,2次合并的得分和=7+13=20.
c(4,3)=1,可知由子序列(4,3)合并成的一堆石子是由第4堆和子序列(5,2)合并而来的.而c(5,2)=1,又表明了子
序列(5,2)的合并方案是第5堆合并第6堆.由此倒推回去,得出第3、第4次合并的方案:
每次合并得分
第三次合并5 4 2……6
第四次合并5 6 ……11
11
子序列(4,3)经2次合并后合并成1堆,2次合并的得分和=6+11=17.
第五次合并是将最后两堆合并成1堆,该次合并的得分为24.
显然,上述5次合并的得分总和为最小
20+17+24=61

 

代码实现:


#include<stdio.h>
#include<iostream>
using namespace std;

#define MAX_LONG 0x7fffffff

struct Node
{   // 当前序列的合并方案
 long c;  // 得分和
 int d;  // 子序列的堆数
};

long sumtype[101][101];  // sumtype[i][j] -子序列[i,j]的石子总数
Node list[101][101];  // list[i][j] -子序列[i,j]的合并方案
int date[101],dt[101];  // date[i] -第i 堆石子数,dt - 暂存date
int i,j,N;   // N -石子堆数, i,j - 循环变量

void Print(int i,int j)  // 递归打印子序列[i,j]的合并过程
{
 int k, x;    // k -循环变量,x - 子序列中首堆石子的序号
 if(j != 1)    // 继续倒推合并过程
 {
  Print(i,list[i][j].d);   // 倒推子序列的合并过程
  x=(i+list[i][j].d-1)%N+1;  // 求子序列中首堆石子的序号
  Print(x,j-list[i][j].d);  // 倒推子序列的合并过程
  for(k=1;k<=N;k++)   // 输出当前合并第i 堆,第x 堆石子的方案
   if(date[k]>0)
    if(i==k || k==x)
     printf("-%d ",date[k]);
    else
     printf("%d ",date[k]);
  printf("/n");
  date[i]=date[i]+date[x];  // 原第i 堆和第x 堆合并成第i 堆
  date[x]=-date[x];   // 将原第x 堆从圈内去除
 }
}
void solve(int s)
{
 int i,j,k;
 long t,x;
 for(i=1;i<=N;i++)     // 仅含一堆石子的序列不存在合并
 {
  list[i][1].c=0;
  list[i][1].d=0;
 }
 for(j=2;j<=N;j++)     // 顺推含堆,含堆……含N 堆石子的各子序列的合并方案
  for(i=1;i<=N;i++)    // 当前考虑从第i 堆数起,顺时针数j 堆的子序列
  {
   if(s==1)    // 合并[i,j]子序列的得分和初始化
    list[i][j].c=MAX_LONG;
   else
    list[i][j].c=0;
   t=sumtype[i][j];   // 最后一次合并的得分为[i,j]子序列的石子总数
   for(k=1;k<=j-1;k++)   // 子序列的石子堆数依次考虑堆……j-1堆
   {
    x=(i+k-1)%N+1;   // 求子序列首堆序号
    if((s==1 && list[i][k].c+list[x][j-k].c+t<list[i][j].c) ||
     (s==2 && list[i][k].c+list[x][j-k].c+t>list[i][j].c))
     // 若该合并方案为目前最佳,则记下
    {
     list[i][j].c=list[i][k].c+list[x][j-k].c+t;
     list[i][j].d=k;
    }
   }
  }
  // 在子序列[1,N],[2,N],……,[N, N]中选择得分总和最小(或最大)的一个子序列
  k=1;
  x=list[1][N].c;
  for(i=2;i<=N;i++)
   if((s==1 && list[i][N].c<x) || (s==2 && list[i][N].c>x))
   {
    k=i;
    x=list[i][N].c;
   }
   Print(k,N);    // 由此出发,倒推合并过程
   printf("%d/n",sumtype[1][N]); // 输出最后一次将石子合并成一堆的石子总数
   printf("/n");
}
int main()
{
 scanf("%d",&N);    // 读入石子堆数
 for(i=1;i<=N;i++)
  scanf("%d",&date[i]);  // 读入每堆石子数
 for(i=1;i<=N;i++)   // 求每一个子序列的石子数sumtype
  sumtype[i][1]=date[i];
 for(j=2;j<=N;j++)
  for(i=1;i<=N;i++)
   sumtype[i][j]=date[i]+sumtype[i%N+1][j-1];
 for(i=1;i<=N;i++)   // 暂存合并前的各堆石子
  dt[i]=date[i];
 solve(1);    // 求得分和最小的合并方案
 for(i=1;i<=N;i++)   // 恢复合并前的各堆石子
  date[i]=dt[i];
 solve(2);    // 求得分和最大的合并方案
 return 0;
}