区间dp—— [NOI1995]石子合并

题目描述

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

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

输入输出格式

输入格式:

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

输出格式:

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

输入输出样例

输入样例#1:  复制
4
4 5 9 4
输出样例#1:  复制
43
54




===========================================================================

题外话: 学习动态规划也有一段时间了,但一直感觉乱乱的,好像这一周里什么都没学会。经常看题解也是看不懂别人在说什么。应该是自己对dp的某些方面存在误区吧,那么接下来的一段时间应该好好总结总结,体会一下dp的思考过程,争取早日真正入门dp。

===========================================================================

  • 题目描述很简单,有n个带权值的石头围成一圈,求将这些石头合并为一堆所得到的最大或最小权值和。
  • 入门思考过程:在题目的分类中,该题目属于区间dp,我们暂且先抛开dp这个概念来思考这个问题,假设现在有3个石头(石头A、B、C)供我们合并,我们应该怎样才能得到最优解。最笨的方法,也是最普遍的方法,我们会列举所有的情况,即分别求出(A+B)+C,A+(B+C),B+(C+A)的值后取最大或最小的结果。(写到这里,突然想起老师上课说的一句话:计算机也是非常笨的。它的优势体现在我们1分钟能做完的事它能在1ms内做完,但其中的工作流程都是一样的),回到题目,假设现在有四块石头A、B、C、D,我们又该怎么做呢?同样我们列举所有的情况:

1、(A+B)+C+D  →  AB+C+D  →  ①(AB+C)+D

                                                  →  ② AB+(C+D)

                                                  →  ③ C+(D+AB)

2、A+(B+C)+D  →  A+BC+D  →  ①(A+BC)+D

                                                  →  ② A+(BC+D)

                                                  →  ③ BC+(D+A)

3、A+B+(C+D)  →  A+B+CD  →以此类推。。。(共3*4==12种情况)

4、B+C+(D +A) →  B+C+DA

      前面我们说到计算机是很笨的,只能我们给它提供一个思路,然后它按照我们所提供的思路不断的重复执行我们之前的计算过程。

        所以说现在的问题是我们应该怎么做,才能让计算机模拟出我们上面所有的运算过程,同时记录所有标红式子(即各个状态)的结果。单独分析 1 ,我们发现对于每一阶段的式子,它的石头个数是不同的:4,3,2,所以石头个数可以作为描述状态的一个量。同时对比1、2、3、4我们发现他们的不同点在于开始时刻的位置是不同的,因此,通过石头个数和开始合并的位置,我们可以确定一个唯一的状态。现在我们已经可以用f[i][j]来表示长度为i,起始位置为j(或长度为j,起始位置为i)的状态了。显然,我们现在必然需要两层for来给每个状态赋值:

for(int i=n;i>=2;--i)

    for(int j=1;j<=i-1;++j)

      现在,我们已经可以枚举每个状态了,那么该怎么给它们赋值呢?在此之前,我们先来思考这么一个问题,我们想要用 f 数组来记录什么,是上述式子整个的值,还是只要括号中的部分就可以了。显然,是后者。所以我们得到如下结论:f[i][j]是记录的是长度为 i 的一组石头 j 与 j+1 合并得到的权值。

f[i][j]=a[j]+a[j+1];

     很明显,该式子只适用于长度为n的情况。还是以n==4为例,我们看当n==3时,f[i][j]=f[i+1][](突然发现自己写不出来。。。)。

既然这样,我们不妨试一下用 i 表示起始位置,用 j 表示长度,即:

for(int  i=1;i<=n;++i)

        for(int j=1;j<=n-i+1;++j)// j 最长就那么长。

显然f[i][j]=a[j]+a[j+1];(长度为n时),那么长度为n-1时呢?f[i][j]=f[i+1][j](又写不出来了,明天继续研究这个问题。。。)

第二天:

分析了一晚上:没有分析出来!!!!!!

好吧,那我们就先看一下一般的正常思路吧,先假设石子是直线型摆放的

        同样,为了能表示并记录所有的状态 ,我们用二维数组 f[i][j] 来表示合并第 i 块石头到第 j 块石头(包括i、j)捡石头的权值和,同样的问题,现在我们应该怎么给这些状态赋值呢?我们还是先借助上述n==4的例子:

1、(A+B)+C+D  →  AB+C+D  →  ①(AB+C)+D

                                                  →  ②AB+(C+D)

2、A+(B+C)+D  →  A+BC+D  →  ①(A+BC)+D

                                                  →  ②A+(BC+D)

3、A+B+(C+D)  →  A+B+CD  →     以此类推。。。

4、B+C+(D +A) →  B+C+DA  →     。。。。。。(共2*4==8种情况)

        我们发现当 i==j 时,f[i][j]=0,花费力气为0;j==i+1时f[i][j]=a[i]+a[j];j==i+2时,此时f[i][j]=(a[i]+a[i+1])+a[j]或a[i]+(a[i+1]+a[j])或a[i+1]+(a[j]+a[i]),(ps:我们发现i+2==j时,我们在求解f[i][j]时用到了 i+1 ;那么同理,当i+3==j时,我们会用到 i+1,i+2;回到i+2==j),我们不妨令k==i+1,那么 显然f[i][j]=f[i][k]+f[k+1][j](这只是两边分别合并时花费的力气,若想得到对应i——j的值,还要加上最后两堆合并时花费的力气,即:a[i]+...+a[k]+a[j]),所以我们得到如下式子 :

f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1].

然后我们还需要比较f[i][j]所有的状态,最终取最优解作为该状态的值。

(可以科普一下什么是前缀和:http://blog.csdn.net/K_rew/article/details/50527287)

        继续回到本题,刚才我们是借助 i+2==j 时的情况得出了上述式子,那么当i+3==j,i+4==j,i+5==j时应该怎么给f[i][j]赋值呢?或者说怎么才能枚举出i+3==j,i+4==j...时的全部状态呢,一种方法就是枚举断点。比如i+3==j时的断点就是i,i+1,i+2(ps:断点是指两个数之间的点,在这里的i,i+1...是指这些点后面的间断处,我们也可以用i+1,i+2,j来表示断点),那问题是为什么枚举断点可以得出所有我们想要的状态呢?可以这么想,还是以i+3==j为例,我们假设此时供我们合并的只有2部分,那我们能得到多少种情况,很明显是n-1种,即枚举断点所得到的所有情况。而被断点所分割的两部分,我们在之前都是求过的它们各自合并时的最优值的,(ps:我们现在记录i+3==j时f[i][[j]所用到的过去的值,并非过去所有状态的值,而是这些状态中的最优解。)所以,我们可以通过枚举断点来表示当前i+3==j时的所有状态,并通过比较这些状态,最后确定这一状态的唯一解,即最优解。

        因此,我们最后得到的核心代码:

//  if(i==j)f[i][j]==0;

for(int i=1;i<=n;++i) for(int i=n;i>=1;--i)//保证后面用到的结果前面已经求出。

        for(int j=i;j<=n;++j)

                for(int k=i;k<=j-1;++k) //  或for(int k=i+1;k<=j;++k)

                     f[i][j]=min(f[i][j],f[i][k]+f[k+1]+sum[j]-sum[i-1]); //  或f[i][j]=min(f[i][j],f[i][k-1]+f[k][j]+sum[j]-sum[i-1]);

cout<<f[1][n];

最后,上述分析是基于石子呈直线分布的,那当石子呈环形分布时,这里提供一种破环成链的方法:

for(int i=1;i<=n;++i){

    cin>>a[i];

    a[i+n]=a[i];

 之后关键代码改为如下形式即可:

//  if(i==j)f[i][j]==0;else f[i][j]=999999999 or -999999999; 

for(int i=1;i<=n*2;++i)  for(int i=2*n;i>=1;--i)

        for(int j=i;j<=n*2;++j)

                for(int k=i;k<=j-1;++k) //  或for(int k=i+1;k<=j;++k)

                     f[i][j]=min(f[i][j],f[i][k]+f[k+1]+sum[j]-sum[i-1]); //  或f[i][j]=min(f[i][j],f[i][k-1]+f[k][j]+sum[j]-sum[i-1]);

cout<<f[1][n];

那么关键是我们为什么可以这么做呢?

    首先,我们先来看一下环状合并与直线型合并有什么区别,比如现在有A,B,C,D四块石头,对于直线型,当起点为B时,我们最大的合并区间无非是f[B][D],而对于环形,我们最大的合并区间是 B→B ,因此我们选择将原数组扩大一倍。

与此同时,我们的起始位置,和终点位置也由原来的1 ~ N变为1 ~ 2N,断点位置则还是i~j。

所以我们得到以下程序(已AC):

#include<bits/stdc++.h>
using namespace std;
int f[101*2][101*2],g[202][202],a[101*2],sum[202]; //数组开双倍大小。
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;++i)
    {
        cin>>a[i];
        a[i+n]=a[i];                                //破环成链的一种方法。
    }
    for(int i=1;i<=n*2;++i)sum[i]=sum[i-1]+a[i];    //注意sum前缀和的赋值要连续,不可用sum[i+n]=sum[i],显然这样是不对的(比如现在有
    for(int i=1;i<=2*n;++i)                         //A,B,C,D四块石头,若当前区间为C~B,状态转移时用上式得到sum[c]-sum[a],显然不合题意。
        for(int j=1;j<=2*n;++j)
        {
            if(i==j)g[i][j]=0;
            else g[i][j]=99999999;                 //一般在求最大值时可默认原数组为0,原题目数据有负数出现除外;求最小值时不可默认为0,
        }                                          //因为最初的f[i][j]一定为0,min结果会一直为0,并可能出现负数。
    int maxx=0,minn=999999999+1;
    for(int i=2*n;i>=1;--i)                        //这里要注意起始位置要逆序枚举。
        for(int j=i;j<=2*n;++j) 
            for(int k=i;k<=j-1;++k)
            {
                f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
                g[i][j]=min(g[i][j],g[i][k]+g[k+1][j]+sum[j]-sum[i-1]);
            }
    for(int i=1;i<=n;++i)                         //该题因为为环形,所以答案可能为f[1][n],也可能是f[2][2+n-1]......
    {
        if(f[i][i+n-1]>maxx)maxx=f[i][i+n-1];
        if(g[i][i+n-1]<minn)minn=g[i][i+n-1];
    }
    cout<<minn<<endl<<maxx;
    return 0;

}

最后,我们再来梳理一下本题的思路:

  • 首先,我们把环状模型变成链状模型,同时求出前缀和,注意前缀和要保持1~2N连续求解。
  • 其次,我们要根据题目要求(比如是求最大还是最小值)确定某些特殊状态的初始值。
  • 然后,一层for逆序枚举起点,一层for顺序枚举终点,再一层for顺序枚举i~j间的断点,并通过状态转移方程f[i][j]=max/min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1])筛选当前状态应有的值。
  • 最后,根据起点的不同,选出最优解然后输出即可。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值