【题目描述】

有n堆石子排成一列,每堆石子有一个重量$w_i$, 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和$w_i + w_{i + 1}$。 问安排怎样的合并顺序,能够使得总合并代价达到最小。

【题目链接】

CodeVS 1048 石子归并 【NOIP 2000】

【解题思路】

区间DP入门题目,设$f[l][r]$表示将区间$[l, r]$合并成一堆的最小代价,则有:

$$ f[l][r] = min\{\ f[l][k] + f[k+1][r]\ \} + sum(l,\ r),k \in [l, r) $$

其中 $sum(l, r) = \sum_{i\ =\ l}^r w_i$

即枚举区间$[l, r]$是由$[l, k]$与$[k + 1, r]$合并而来。

边界条件: $$f[i][i] = 0$$

注意转移的顺序,既不是按左端点枚举也不是按右端点枚举。

考虑我们计算一个区间时需要哪些区间的信息——比当前区间长度短的区间!

故应按照区间长度由短到长计算,这也是区间型DP的一般特征。

状态 $O(n^2)$ ,转移 $O(n)$ ,总时间复杂度为 $O(n^3)$。

【AC代码】

#include <cstdio>
#include <climits>
#include <algorithm>

#define MAXN 100

int a[MAXN], n;
int f[MAXN][MAXN];

inline int sum(int l, int r){
    if(l == 0) return a[r];
    else return a[r] - a[l - 1];
}

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        scanf("%d", a + i);
        if(i) a[i] += a[i - 1];
    }

    for(int i = 0; i < n; i++) f[i][i] = 0;
    for(int len = 1; len < n; len++){
        for(int l = 0; l < n - len; l++){
            int r = l + len;
            int min = INT_MAX;
            for(int k = l; k < r; k++){
                min = std::min(min, f[l][k] + f[k + 1][r]);
            }
            f[l][r] = min + sum(l, r);
        }
    }

    printf("%d\n", f[0][n - 1]);
    return 0;
}

【扩展:环形石子归并】

考虑一个小扩展,即石子不是排成一列而是成环形,该如何处理呢?

我们可以枚举断点然后展开计算,不过这样复杂度要多上一个 $O(n)$。

更好的做法是将环展开成链,并且翻倍,这样以 $i$ 为断点环即对应了序列上区间 $[i, i + n)$,像上面一样计算即可。 时间复杂度$O(n^3)$。

【环形石子归并代码】

CodeVS 2102 石子归并2

#include <cstdio>
#include <climits>
#include <algorithm>
#include <cstring>

#define MAXN 100

int a[MAXN * 2], n;
int preSum[MAXN * 2];
int f[MAXN][MAXN], g[MAXN][MAXN];
int minSocre = INT_MAX, maxSocre = INT_MIN;

inline int sum(int l, int r, int start){
    return preSum[r + start] - preSum[l + start - 1];
}

inline void solve(int start){
    memset(f, 0, sizeof f), memset(g, 0, sizeof g);
    for(int len = 1; len < n; len++){
        for(int l = 0; l < n - len; l++){
            int r = l + len;
            int min = INT_MAX, max = INT_MIN;
            for(int k = l; k < r; k++){
                min = std::min(min, f[l][k] + f[k + 1][r]);
                max = std::max(max, g[l][k] + g[k + 1][r]);
            }
            f[l][r] = min + sum(l, r, start);
            g[l][r] = max + sum(l, r, start);
        }
    }

    minSocre = std::min(minSocre, f[0][n - 1]);
    maxSocre = std::max(maxSocre, g[0][n - 1]);
}

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        scanf("%d", a + i);
        a[i + n] = a[i];
    }

    preSum[0] = a[0];
    for(int i = 1; i < 2 * n; i++) preSum[i] = preSum[i - 1] + a[i];

    for(int i = 0; i < n; i++) solve(i);

    printf("%d %d\n", minSocre, maxSocre);

    return 0;
}

【留坑】

石子归并还有名为GarsiaWachs的解法,借助平衡树可将时间复杂度优化到$O(nlogn)$。

然而留坑待填 ...

就先这样子啦。