石子合并(弱化版)

GitHub跳转原题关系图返回列表

从指数级枚举合并顺序的暴力出发,利用相邻合并对应连续区间的性质,设区间 dp[l][r] 枚举最后一次合并的断点,用前缀和加速得到 O(n³) 区间 DP。

OJ: luogu

题目 ID: P1775

难度:普及

标签:动态规划区间dp前缀和

创建: 2026-06-19 18:22

更新: 2026-10-24 00:35

后置题目
luogu P3205[HNOI2010] 合唱队B 复用 A 的「dp[l][r] 连续区间子问题、按区间长度递增递推」这一步区间 DP 框架,再叠加最后插入者在左端/右端的 0/1 维度与向两端扩张的计数转移luogu P1063[NOIP 2006 提高组] 能量项链B 原样复用 A 教的「只关心最后一次合并断在哪里、dp[l][k]+dp[k+1][r] 按区间长度递增递推」这一步(同一断点分解与长度序),只是把代价换成三标签乘积取 max 并叠加 A 未教的环断链复制。luogu P1880[NOI1995] 石子合并B 的链上区间 DP 直接复用 A 教的枚举最后一次合并断点 dp[l][r]=min(dp[l][k]+dp[k+1][r]+sum) 与按长度递增递推,再叠加破环成链与 min/max 双答案roj 3171能量项链B 的「能量项链」主解正是复用 A 教的「枚举最后一步切点 k、左右子区间各自最优再合并」这一步区间 DP,在 B 中把断点代价从区间石子质量之和换成 a_i·a_{k+1}·a_{j+1},再叠加环拆链复制一倍取 N 个窗口最大值,所以 A 是 B 的直接前置台阶。roj 3548[NOIP2006-提高] 能量项链A 教的『最后一次合并必在断点 k 处把区间劈成两半』正是 B 的 f(i,j)=max{f(i,k)+f(k+1,j)+代价} 这一步,B 只是把代价换成 a_i*a_(k+1)*a_(j+1),再叠加环拆链复制一倍、对 N 个长度 N 窗口取最大。roj 3526加分二叉树B 的前序还原解法直接复用 A 教的区间 DP 步骤:同样把连续区间 [l,r] 当作状态、枚举断点 k 把区间劈成左右两段独立子问题,代码 `best(l,r)` 里 `for k in range(l, r+1)` 分成 [l,k-1]/[k+1,r] 与 A 的 dp[l][k]+dp[k+1][r] 是同一状态设计与断点枚举,B 只是在其上叠加“记录最优根并栈式还原前序遍历”这一层新流程。roj 3501[noip2000]乘积最大B 的转移 (dp[j][i]=max(dp[j-1][t]*num(t+1,i))) 直接复用 A 教的关键观察——枚举最后一次操作的位置、把序列切成前缀子问题加末段:A 枚举最后一次合并的断点 k 得区间 dp,B 枚举最后一个乘号的位置 t 得分层 dp[j][i],只把区间状态换成(乘号数,前缀长度)并把区间和改成大整数乘法。roj 1580「一本通 5.2 练习 1」加分二叉树P1775 用「区间状态 + 枚举断点劈成左右两段 + 按长度递推」的区间 DP 合并石子,1580 直接搬用同一劈区间模板,只把断点换成根、区间和换成因子相乘roj 1574「一本通 5.1 练习 3」矩阵取数游戏B 逐行做的区间 DP 直接复用 A 教的两步:把 dp[l][r] 设计成「连续子区间子问题」的状态、再按区间长度(段长)递增递推保证子段先算出(B 代码 for length in range(2, m+1)),只是把 A 的最后一次合并断点转移换成取行首/行尾两候选,并叠加行间独立、权重 2^pick 与 Python 大整数。roj 1569「一本通 5.1 例 1」石子合并B 的环形石子合并整段照搬 A 教的“最后一次合并断点 k”区间 DP 转移,只是在外层再叠加复制成链并枚举长度 n 的窗口来破环roj 1571「一本通 5.1 例 3」凸多边形的划分B 把 A 教的“区间状态 + 枚举最后一次断点把区间劈成两段子问题再取 min”整套区间 DP 骨架搬到凸多边形三角剖分上,只把最后的合并代价 sum(l,r) 换成三角形乘积 w_i*w_k*w_j,难度的增量在高精度与剖分建模而非新范式。roj 1197山区建小学B 的区间划分类 DP 直接复用 A 教的「按最后一步把区间拆成 [l,k] 与 [k+1,r] 枚举断点」这一转移骨架(dp[前段]+段代价取 min、段代价用前缀和求),只是把「合并成一堆的代价」换成分好的「段内到中位村的距离和 cost(l,r)」,并额外叠加 A 未教的连续分段等价性与中位村选址两步新观察。luogu P1622释放囚犯B 沿用 A 的区间 dp[l][r] 枚举分割点、子区间独立合并的区间 DP 骨架,把石子合并的最后断点换成第一个释放的囚犯luogu P3146[USACO16OPEN] 248 GB 复用 A 的区间 DP 枚举最后一次断点把区间拆成两段转移这一具体步骤,只是把代价和换成两边相等才合并的取值规则OpenJ_Bailian 1651Multiplication PuzzleB 直接复用 A 教的「区间 DP 倒推最后一步、枚举断点 k、按区间长度递增递推」这一步(dp[i][j]=min_k dp[i][k]+dp[k][j]+cost),只是把最后一次合并换成了最后一张被取走的卡片并改计 A[i]*A[k]*A[j]。

形式化题目

给定一排 NN(N≤300N \le 300)堆石子,第 ii 堆质量为 mim_i(mi≤1000m_i \le 1000)。每次操作选择相邻的两堆合并成一堆,新堆质量为两堆之和,代价也为两堆之和;新堆与原来两侧的堆保持相邻。把所有石子合并成一堆所需的总代价由合并顺序决定,求所有合法合并顺序中总代价的最小值。

暴力解法

思路

最直接的做法完全按题意模拟:每一轮枚举当前序列中所有相邻的位置,尝试把这一对合并掉,然后递归处理剩下的序列,直到只剩一堆为止。递归到边界时统计一路上付出的代价和,在所有分支里取最小值。

设 dfs(stones) 表示把当前序列 stones 合并成一堆的最小总代价。每一层枚举 ∣stones∣−1|stones|-1 个合并位置,序列长度每层减一,总状态是所有可能的「中间序列」,数量是指数级。

text
dfs(stones):
    若 stones 只剩一堆,返回 0
    对每个相邻位置 i:
        合并 stones[i] 与 stones[i+1],代价为二者之和
        递归 dfs(合并后的序列)
    返回所有分支中「本次代价 + 后续代价」的最小值

这个写法覆盖了每一种合法的合并顺序,因此结果一定正确;但指数级的搜索规模决定了它只能处理很小的 NN,正式提交会超时。它的价值是给出一个可信的基线,用来检验后面的优化。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

int dfs(vector<int> stones) {
    if (stones.size() == 1) {
        return 0;
    }

    int best = INT_MAX;
    for (int i = 0; i + 1 < (int)stones.size(); ++i) {
        vector<int> next = stones;
        int merged = next[i] + next[i + 1];
        next[i] = merged;
        next.erase(next.begin() + i + 1);
        // 暴力枚举这一步选择哪一对相邻石子合并
        best = min(best, merged + dfs(next));
    }
    return best;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> stones(n);
    for (int i = 0; i < n; ++i) {
        cin >> stones[i];
    }

    cout << dfs(stones) << '\n';
    return 0;
}

复杂度

每一层分支数约为当前堆数,递归深度为 NN,总状态数是卡特兰数量级,时间复杂度是指数级;NN 到十几就会明显跑不动。

瓶颈

暴力在每一步都关心「当前每堆的质量分布」,也就是整个序列现在的形态。但真正决定答案的结构被藏在了形态背后:无论中间过程如何,每一步合并的都是原序列某段连续区间,最终某次合并一定是把某段连续区间的左边半段和右边半段各合成一堆,再合并这两堆。

也就是说,同一个区间 [l,r][l, r] 被多少种不同顺序合并成一堆,对上层来说都只需要知道一个数:合并它的最小代价。暴力把大量「顺序不同但子区间相同」的搜索分支重复展开,这就是被消除的瓶颈。

正解

思路

把观察写成状态:设 dp[l][r] 表示把原序列中区间 [l,r][l, r] 内的石子合并成一堆的最小代价。区间 [l,r][l, r] 的最后一次合并,一定是把 [l,k][l, k] 和 [k+1,r][k+1, r](l≤k<rl \le k < r)各自合并成的两堆再合并一次,代价为区间总和 sum(l,r)sum(l, r)。于是:

dpl,r=min⁡l⩽k<r(dpl,k+dpk+1,r)+sum(l,r) dp_{l,r}=\min_{l\leqslant k<r}\left(dp_{l,k}+dp_{k+1,r}\right)+sum(l,r)

注意 sum(l,r)sum(l, r) 是定值,与断点 kk 无关,可以移出 min⁡\min。长度为 1 的区间不需要合并,dpi,i=0dp_{i,i}=0。答案就是 dp1,ndp_{1,n}。

下面用样例 m=[2,5,3,1]m = [2, 5, 3, 1](前缀和为 0,2,7,10,110,2,7,10,11)列出几个区间的计算过程,行是区间,最后一列是答案:

区间 [l,r][l,r] 区间和 枚举的断点 kk 与取值 dpl,rdp_{l,r}
[2,3][2,3] 88 k=2k=2:dp2,2+dp3,3=0dp_{2,2}+dp_{3,3}=0 88
[3,4][3,4] 44 k=3k=3:dp3,3+dp4,4=0dp_{3,3}+dp_{4,4}=0 44
[2,4][2,4] 99 k=2k=2:8+0=88+0=8;k=3k=3:0+4=40+4=4 1313
[1,4][1,4] 1111 k=1k=1:0+dp2,4=130+dp_{2,4}=13;k=2k=2:dp1,2+dp3,4=7+4=11dp_{1,2}+dp_{3,4}=7+4=11;k=3k=3:dp1,3+0=15dp_{1,3}+0=15 2222

读表时注意两点。第一,区间和 sum(l,r)sum(l,r) 是「最后一次合并」付出的代价,这一步把区间内所有石子重新压成一堆,所以每个 dpl,rdp_{l,r} 都要完整加上它一次。第二,短区间的 dpdp 值是长区间枚举断点时的原材料,所以三重循环必须按区间长度从短到长递推,保证算 dpl,rdp_{l,r} 时所有更短的子区间已经算好——这也是区间 DP 的通用遍历顺序。

区间和用前缀和 O(1)O(1) 求出:sum(l,r)=sumr−suml−1sum(l,r) = sum_r - sum_{l-1},这里直接复用了前缀和这一基础技巧。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> a(n + 1), sum(n + 1, 0);
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }

    const int INF = 0x3f3f3f3f;
    vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));

    // dp[l][r] 表示把区间 [l, r] 合并成一堆的最小代价
    for (int len = 2; len <= n; ++len) {
        for (int l = 1; l + len - 1 <= n; ++l) {
            int r = l + len - 1;
            dp[l][r] = INF;
            int seg_sum = sum[r] - sum[l - 1];
            for (int k = l; k < r; ++k) {
                dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + seg_sum);
            }
        }
    }

    cout << dp[1][n] << '\n';
    return 0;
}

复杂度

三重循环分别枚举区间长度、左端点和断点,时间复杂度 O(n3)O(n^3);dpdp 表是二维的,空间复杂度 O(n2)O(n^2)。本题 n≤300n \le 300,O(n3)≈2.7×107O(n^3) \approx 2.7 \times 10^7 次转移,轻松通过。

总结

这题的核心不是「怎么模拟合并」,而是识别出「只能合并相邻两堆」意味着每堆石子在任意时刻都对应原序列的一段连续区间。把视角从「枚举每一步操作」换成「枚举最后一次合并的断点」后,指数级的搜索就被折叠成了 O(n2)O(n^2) 个区间子问题,配合前缀和得到 O(n3)O(n^3) 的区间 DP。暴力解法虽然上不了评测机,但它给出了可信的答案来源:用随机小数据与正解对拍,是区间 DP 实现出错时最直接的排错手段。

图示解析

一图流解析把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析