算法设计与分析 SCAU19182 石子合并(基础版)

19182 石子合并(基础版)

时间限制:1000MS 代码长度限制:10KB
提交次数:0 通过次数:0

题型: 编程题 语言: G++;GCC;VC;JAVA

Description

设有 N(N≤300) 堆石子排成一排,其编号为1,2,3,⋯,N。每堆石子有一定的质量 mi(mi≤1000)。
现在要将这N堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。
合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。


输入格式

第一行,一个整数 N。

第二行,N 个整数 mi。

输出格式

输出仅一个整数,也就是最小代价。(题目确保答案在int范围)


输入样例

4
2 5 3 1


输出样例

22


解题思路

1. dp 方程定义
  • dp[l][r] 表示从 L 到 R 合并成一堆的最小代价


2. 状态转移方程

此题 dp 思路是将所有区间以及需要的代价全部列出来,那么如何列出来呢?

  1. 对于 dp[l][r]:从 L 到 R 合并成一堆的最小代价,我们需要一个指针 mid,放在区间 [l, r] 中间,为什么需要这个 mid 呢?

目的就是将区间按多种情况来进行分割,以找到最符合要求的情况

因此转移方程可以为:dp[l][r] = min(dp[l][r], dp[l][mid] + dp[mid + 1][r] + s[r] - s[l - 1]);

  1. dp[l][mid] + dp[mid + 1][r] + s[r] - s[l - 1] 的意义
  • 先把区间 [l, r] 切分为两部分 [l, mid] 和 [mid+1, r],mid 是切分点。
  • 再把两部分 [l, mid] 和 [mid+1, r] 合并在一起。
  • 注意在进行上一步的同时,还要加上 s[r] - s[l - 1],也就是合并的代价(通过前缀和来求,即区间和,这段区间加起来的总和)。

在这里插入图片描述

  1. 转移方程的初值:dp[i][i] = 0,因为合并每堆石子的代价为0,即自己跟自己合并相当于不用合,即最小代价为0

  2. 转移方程的最终结果:dp[1, n]:求合并第1堆到第 n 堆石子的最小代价

3. 算法解题思路
  1. 对 dp 数组进行初始化,每个值都初始化为较大的一个数 memset(dp, 0x3f, sizeof(dp));
  2. 首先进行数据输入,输入的同时记录前缀和 s[i] = s[i - 1] + a[i]; 以及给转移方程设立初始值 dp[i][i] = 0;
  3. 进行状态转移,我们目标是列出所有情况,即各个区间代价和。第一层循环变量为 len:区间长度,第二层循环为 l:区间起点,第三层循环为 mid,区间的分割点位置
  4. 每次循环时进行决策,跟之前算出过的代价和比较哪个更小,dp[l][r] = min(dp[l][r], dp[l][mid] + dp[mid + 1][r] + s[r] - s[l - 1]);



更多注释可查看下方的完整代码中,有助于理解

代码如下
#include <iostream>
#include <stdio.h>
#include <cstring>

using namespace std;

int a[301]; // 记录各石堆质量
int s[301]; // 前缀和数组
int dp[301][301]; // dp[i][j] 代表第 i 到 第 j 堆石子的最小代价

int main()
{
    memset(dp, 0x3f, sizeof(dp));//先把 dp 里的所有数变成一个很大的数

    int i, n;
    cin >> n;

    s[0] = 0; // 初始化用于后续记录

    for(i = 1; i <= n; i++) {
        cin >> a[i];
        s[i] = s[i - 1] + a[i]; // 记录前缀和
        dp[i][i] = 0; // 自己跟自己合并相当于不用合,即最小代价为0
    }

    int len, l, r, mid;

    // len 为区间长度,l(left) 为区间起点,r(right) 为区间终点
    for(len = 2; len <= n; len++) {
        // l的终点为最后一段区间的起点
        for(l = 1; l <= n - len + 1; l++) {
            r = l + len - 1;

            // mid将区间 [l, r] 分成左右两段,所以 mid 不需要等于 r(分不了了)
            for(mid = l; mid < r; mid++) {
                // s[r] - s[l - 1] 为区间 [l, r] 的前缀和,即加起来的总和,合并的代价
                dp[l][r] = min(dp[l][r], dp[l][mid] + dp[mid + 1][r] + s[r] - s[l - 1]);
                //printf("dp[%d][%d]=%d, dp[%d][%d]=%d, dp[%d][%d]=%d s=%d\n", l, r, dp[l][r], l, mid, dp[l][mid], mid + 1, r, dp[mid + 1][r], s[r] - s[l - 1]);
            }
        }
        //printf("len=%d\n\n", len);
    }

    cout << dp[1][r] << endl;
    return 0;
}


最后

对我感兴趣的小伙伴可查看以下链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值