石子合并 (区间DP)

【问题描述】

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

【输入文件】

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

【输出文件】

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

【输入样例】

3

1 2 3

【输出样例】

9 11

【问题分析】

人们那到这类题目的第一想法是贪心(找最大/最小的堆合并),但是很容易找到贪心的反例:(以求最小为例)

 

贪心:

3  4  6  5 4  2

5  4  6  5 4        得分:5

9  6  5 4           得分:9

9  6 9             得分:9

15 9               得分:15

24                  得分:24

总得分:62

合理方案

3  4  6  5 4  2

7  6  5  4 2        得分:7

7  6  5 6           得分:6

7  11 6             得分:11

13 11              得分:13

24                  得分:24

总得分:61

也就是说第二个4 和2 合并要比和5合并好,但是由于一开始2被用了没办法它只好和5合并了……

既然贪心不行,数据规模有很大,我们自然就想到用动态规划了。

*不过要知道某个状态下合并后得分最优就需要知道合并它的总得分,只有对子问题的总得分进行最优的判断,设计的状态才有意义。

*但要是想知道总分,就要知道合并一次的得分,显然这个含义不能加到动态规划的状态中,因为一般一个状态只代表一个含义(也就是说OPT[I]的值只能量化一个问题)(上面代*号的两段比较抽象,不懂可以不看。我只是为了标注自己的理解加的,不理解的也没必要理解。)

先要把题目中的环变成链,这样好分析问题。具体方法:把环截断,复制一份放到截断后形成的链的后面形成一个长度是原来两倍的链(只有环中的元素在处理时不随着变化,就可以这样做。其实读入数据已经帮你截断了);

例: 3 4 5

变成 3 4 5 3 4 5

对于这样一个链,我们设计一个状态opt[i,j]表示起点为i终点为j的链合并成一堆所得到的最优得分。

要合并一个区间里的石子无论合并的顺序如何它的得分都是这个区间内的所有石子的和,所以可以用一个数组sum[i]存合并前i个石子的得分。

因为合并是连续的所以决策就是把某次合并看作是把某个链分成两半,合并这想把两半的好多堆分别合并成一堆的总得分+最后合并这两半的得分;

状态转移方程:

maxopt[i,j]=max{maxopt[i,k]+maxopt[k+1,j]}+sum[j]-sum[i-1]

minopt[i,j]=min{minopt[i,k]+minopt[k+1,j]}+sum[j]-sum[i-1]

复杂度:状态数O(N2)*决策数O(N)=O(N3)


#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int INF = 999999999;
int num[300], sum[300], n;
int dpmx[300][300], dpmn[300][300];

int main()
{
    while(scanf("%d",&n) != EOF)
    {
        int i, j, k, total;
        for(i = 0; i < n; i++)
        {
            scanf("%d",&num[i]);
            num[i+n] = num[i];  //将环化成链处理!
        }

        sum[0] = num[0];
        for(i = 1; i < 2 * n; i++)
            sum[i] = sum[i-1] + num[i];

        for(i = 0; i < 2 * n; i++)
            for(j = i; j < 2 * n; j++)
                dpmx[i][j] = -INF, dpmn[i][j] = INF;

        for(i = 0; i < 2 * n; i++)
            dpmx[i][i] = dpmn[i][i] = 0;

        for(k = 1; k < n; k++)
        {
            for(i = 0; i + k < 2 * n; i++)
            {
                for(j = i; j < i + k; j++)
                {
                    if(i == 0) total = sum[i+k];
                    else total = sum[i+k] - sum[i-1];
                    dpmx[i][i+k] = max(dpmx[i][i+k], dpmx[i][j] + dpmx[j+1][i+k] + total);
                    dpmn[i][i+k] = min(dpmn[i][i+k], dpmn[i][j] + dpmn[j+1][i+k] + total);
                }
            }
        }

        int retmx = -INF;
        int retmn = INF;
        for(i = 0; i + n - 1 < 2 * n; i++)
        {
            retmx = max(retmx, dpmx[i][i+n-1]);
            retmn = min(retmn, dpmn[i][i+n-1]);
        }
        printf("%d\n%d\n",retmn,retmx);
    }
    return 0;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值