AcWing 282. 石子合并

题目传送门

一、题目理解

先来理解一下题意:\(1\ 3\ 5\ 2\) 这个例子,

\(A\)方案:

(1) 第一轮 \(1+3=4\) 变成 \(4\ 5\ 2\)

(2) 第二轮 \(4+5=9\) 变成 \(9\)

(3) 第三轮 \(9+2=11\)

最小代价:\(4+9+11=24\)

\(B\)方案:

(1) 第一轮 \(1+3=4\) 变成 \(4\ 5\ 2\)

(2) 第二轮 \(5+2=7\) 变成 \(4\ 7\)

(3) 第三轮 \(4+7\)=\(11\)

最小代价:\(4+7+11=22\)

可以看出,不同的组合方式,确实最终的代价是不一样的。

二、思考过程

1、尝试一维描述

万物皆\(dp\),一切状态都是由前面的状态转化而来。那么,如果用一维的方式来描述前面的状态,就是这样:\(f[i]\)。描述什么呢?似乎应该是从第\(1\)堆到第\(i\)堆的最小代价,如果能行的通,没有人会愿意把问题复杂化,再整什么二维概念了。

下面采用实例法描述一下最简问题,看看这样设计的状态描述办法会不会有问题:

  • 如果有\(1\)堆石子,那么\(f[1]=0\)。为啥呢?一堆石子和谁合并啊,没的合并哪来的代价呢?

  • 如果有\(2\)堆石子,那么\(f[2]=a[1]+a[2]\)。为啥呢?一共两堆,不合并它们两个还能合并谁,代价也没啥最不最小了,就是两堆石子的个数和。

  • 如果有\(3\)堆石子,那么就有点意思了,可以\([(1)+(2)]+(3)\),也可以\((1)+[(2)+(3)]\),这时用\(f[i]\)怎么表示呢?我们发现,此时无法描述出\([(1)+(2)]\)\([(2)+(3)]\),这是一个的概念,一维的描述不出来!

2、尝试二维描述

现在来思考二维的描述法,比如\(f[i][j]\),描述就是从\(i\)堆到\(j\)堆的最小代价,依然是用最简例子思考一下:

  • 如果有\(1\)堆石子,那么\(f[1][1]=0\)

  • 如果有\(2\)堆石子,那么\(f[1][2]=a[1]+a[2]\)

  • 如果有\(3\)堆石子,\([(1)+(2)]+(3)\),也可以\((1)+[(2)+(3)]\)

$f[1][3]=f[1][2]+f[2][3] + a[1]+a[2]+a[3] $

为啥最后要加上\(a[1]+a[2]+a[3]\)呢?因为最终两堆合并时,本轮产生的代价就是两堆的石子和啊。

三、状态转移方程

\(i\)\(j\)区间内合并的最小代价,其实是在引入\(k\),其中\(i<=k<j\)后,考查每一个\(k\)作为中间结点来合并\(i\sim j\)的最小代价。

每一轮合并的代价增加值=区间内的石子数量。

如果每次都重新计算区间内的石子数量,性能太差,我们可以使用前缀和来优化。

\(f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1])\)

四、dfs实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 1e3 + 10;
const int INF = 0x3f3f3f3f;
int a[N];
int f[N][N];

int dfs(int l, int r) {
    int &v = f[l][r];
    if (l == r) return v = 0;
    if (v != INF) return v;
    for (int i = l; i < r; i++)
        v = min(v, dfs(l, i) + dfs(i + 1, r) + a[r] - a[l - 1]);
    return v;
}

int main() {
    int n;
    cin >> n;
    //前缀和
    for (int i = 1; i <= n; i++) cin >> a[i], a[i] = a[i - 1] + a[i];
    memset(f, 0x3f, sizeof(f));
    cout << dfs(1, n) << endl;
    return 0;
}

五、DP实现代码

#include <bits/stdc++.h>

using namespace std;

const int N = 3010;
const int INF = 0x3f3f3f3f;

int n;
int s[N];//前缀和
int f[N][N];//表示将i到j合并成一堆的方案的集合,属性:最小值
// 石子合并
int main() {
    cin >> n;
    //一维前缀和,为了计算sum(i,j)的值
    for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];

    memset(f, 0x3f, sizeof f);  //预求最小,先设最大
    for (int i = 1; i <= n; i++)f[i][i] = 0;//第i堆与第i堆是不需要合并的,代价为0

    // 长度为1的区间不用枚举,区间长度是1的话,表示只有一堆,一堆不需要合并,代价是0
    for (int len = 2; len <= n; len++)           //枚举区间长度
        for (int i = 1; i + len - 1 <= n; i++) { //枚举区间左端点
            int j = i + len - 1;                 //区间的右端点
            //最后一个中间点K,需要枚举
            for (int k = i; k < j; k++)
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
        }
    cout << f[1][n] << endl;
    return 0;
}

六、精讲视频

https://www.bilibili.com/video/av329239536/

原文地址:https://www.cnblogs.com/littlehb/p/15457225.html