合并石子

71 阅读7分钟

前言

作者,在近几天学习了合并石子,特地来写一篇博客加深印象和理解。本文章有两个目的意在帮助入门的同学学习和借鉴,同时帮助作者及其其他读者复习。

任意合并

题意

有N堆石子,现要将石子有序的合并成一堆,规定如下:每次只能移动任意的2堆石子合并,合并花费为新合成的一堆石子的数量。

分析

这种情况,属于石子合并里面最简单的一种了。其实只需要贪心的找两堆最小的进行合并,计算合并的代价。本质上是哈夫曼树的变形,可以使用优先队列实现。

给出题目链接P1090 合并果子

参考代码

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>


int n;
std::priority_queue<int , std::vector<int> , std::greater<int>> heap;

int main(){

    std::ios::sync_with_stdio(0);
    std::cout.tie(0);
    std::cin.tie(0);

    std::cin >> n;
    for (int i = 1; i <= n; i ++ ){
        int x;std::cin >> x;
        heap.push(x);
    }

    int ans = 0;
    while(heap.size() != 1){
        int h1 = heap.top();
        heap.pop();

        int h2 = heap.top();
        heap.pop();

        ans += h1 + h2;

        heap.push(h1 + h2);
    }

    std::cout << ans << '\n';

    return 0;
}

相邻两堆进行合并

题意

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

分析

考虑贪心做法 , 但是我们发现使用上述思路贪心做法似乎不正确。 非常容易就可以举出反例:

4
2 5 3 7
  1. 使用贪心做法
    (2 5) 3 7 ----> (7 3) 7 ----> (10 7) ----> 17 代价是 34

  2. 枚举出一种反例
    2 5 (3 7) ----> 2 (5 10) ----> (2 15) ----> 17 代价是 32

所以我们可以hack 掉这个贪心思路,观察题目N最大只有300 , 可以考虑记忆化搜索或者动态规划

事实上,我有注意到存在记忆化搜索的思路,但是因为这里主要是介绍动态规划的思路,所以暂不提其他思路。

如何考虑动态规划的思路

一般动态规划问题都是考虑将大问题转化成小问题,小问题解决了,大问题使用与解决小问题相似的做法也可以解决。

  1. 首先先定义状态数组 f[l][r] 表示的是区间[l , r]之间的石子合并的最小代价。
  2. 然后考虑小问题扩展到大问题的方式
    • 先枚举所有的两堆相邻石子合并的最小代价 , 再枚举所有的三堆相邻石子最小代价以此类推下去。我们可以一直递推出将n堆石子合并的最小代价。
    • 对于初始化,因为我们需要求最小值,所以我们要将数据全部初始化成INF ,因为f[i][i] 代表的是第i个石子和第i个石子合并的含义,明显它为0。

给出实例

样例我们以上面给出样例作为参考

  • 初始的数组

image.png

  • 合并两堆石子 image.png

对应的数组中的情况是这样的

image.png

  • 合并三堆石子 image.png

对于这里,我们需要讨论一下就绿线的 2 5 3 为例


(2 5) 3 ----> (7 3) -----> 10 代价17
2 (5 3) -----> (2 8) ----> 10 代价18

所以我们发现不同的合并方式会有不同的答案,所以我们需要从这些方案中取一个min

对应的数组 image.png

对于合并四个的也是同理,给出对应的数组

image.png

但是对于求出i堆石子合并求出最小值描述并不是特别清楚,所以为了弥补这个缺陷,作者将给出一个更加清楚的方式

我们以下面的样例为例 image.png 对于合并4堆石子,我们该如何考虑?

  • 首先考虑4堆石子我们有哪些合并方式呢?
    (1 , 3) (2 , 2) (3 , 1) 对吧?

使用图示就是这样的

image.png 隔板的左边和右边合并 , 我们发现隔板的左边为 2 5 3 , 2 5 的合并最小代价,前面我们已经求出来了。隔板的右边,5 3 7 , 3 7 的合并最小代价,合并两个石子的时候,合并三个石子的时候,我们也求出来了。 所以我们对于这些数据,我们直接求出取来用即可,在一个一个对比几种方案的最小值。

image.png 我们将绿线称之为窗口,这个窗口内的最小值求出来了,那么窗口向右移。 类似的求法

#include <iostream>
#include <cstring>
#include <algorithm>

#define LL long long

const int N = 1010;

int n;
int a[N] , s[N];
int f[N][N];

void init(){

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            f[i][j] = 0x3f3f3f3f;
}

int main(){

    std::ios::sync_with_stdio(0);
    std::cin.tie(0);
    std::cout.tie(0);

    std::cin >> n;
    init();
    for (int i = 1; i <= n; i ++ ) {
        std::cin >> a[i];
        s[i] = s[i - 1] + a[i];
        f[i][i] = 0;
    }

    for (int len = 2; len <= n; len ++ )
        for (int l = 1; l <= n - len + 1; l ++ ){   // [l , r] 是一个长度为 len 的窗口
            int r = l + len - 1;
            for (int k = l; k < r; k ++ )               // 在 len 个元素中间插入隔板
                f[l][r] = std::min(f[l][r] , f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }

    std::cout << f[1][n] << '\n';

    return 0;
}   

细节解读

  1. 维护窗口的代码

    • for (int l = 1; l <= n - len + 1; l ++ )

    这一段代码 , 维护的是窗口的左端点 ,长度为len的窗口 , 以l为起点 区间为[l,l+len1][l , l + len - 1] , l 最多枚举到 n - len + 1.

  2. 插入隔板的代码

    • for (int k = l; k < r; k ++ ) 在[l , k] 和 [k + 1] 之间插入一块隔板,表示两个区间内的石子堆合并
  3. 状态转移方程 f[l][r] = std::min(f[l][r] , f[l][k] + f[k + 1][r] + s[r] - s[l - 1]); f[l][k] + f[k + 1][r] , 表示的是 f[l][k] 代表[l , k] 这个区间的石子堆合并的最小代价 , f[k + 1][r] 类似。但是这两个加起来,还只是得到两个区间的最小代价,(为了方便理解,你可以将区间石子堆直接看成两堆石子。因为合并成一个区间的最小代价已经找到了,那么就固定了。所以可以直接看成两堆石子合并。) 这两个区间合并,还是需要加上 [l,r] 这个区间石子堆和的代价。

  4. 作者自己想到的问题

  • 问题

为什么合并的时候,已经得到左边 f[l][k] 和 右边 f[k + 1][r] , 为什么要加上整个区间s[r] - s[l-1]。而不是再加上一遍f[l][k]+f[k+1][r]f[l][k] + f[k + 1][r] , 因为得到左边区间的代价是f[l][k] , 得到右边区间的代价是f[k + 1][r]。那两个区间合并不是左边+右边吗?

  • 答案 我们搞混了概念, f[l][r] 是指定 [l , r] 这个区间内石子合并的最小代价 , 是累计了很多小区间合并的代价的,比如 [1 , 4] 这个区间 ,假设是由这个区间[1 , 1] , [2 , 4] 合并得到 , 那么我们将 f[l][k] + f[k + 1][r] 算两遍是不是算了很多重复的子区间代价。换句话说,因为[l , r] 是通过小区间合并累计的代价和的最小值,s[r] - s[l - 1] 单纯是[l , r] , 石子堆的和。所以我们考虑的时候,是要将合并成[l , k] 和 [k + 1 , r] 最小代价 + [l , r]区间石子堆的和 作为我们合并成区间[l , r] 的最小值。

练习题 P1775 石子合并(弱化版)

环型石子合并

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

image.png

这样的情况,我们考虑破环为链 ,然后对每一个点都将当作起点。

image.png

这样就直接转化成对一条链求最小合并代价了。 但是区间的最大长度是n , 即 f[1][n]f[1][n]。 只需要将

for (int l = 1; l <= n - len + 1; l ++ )

nlen+1n-len + 1 写成 2n2 * n 即可

学完可以去刷这个题石子合并

总结

合并石子是区间dp 的典题,作者依旧感觉自己的dp 没有学明白,这个篇文章写的也不是很清楚。

如果读者任何建议,请随时评论