C区间 DP

石子合并(链形)

区间合并基础模型

本课摘要

石子合并(链形)课程回答“区间合并代价为何要按长度递增计算”。内容以区间合并基础模型为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断石子合并(链形)的适用条件与状态边界
  • 围绕“区间合并基础模型”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

把相邻的石子并成一堆

先看一个具体场景:一排摆着 4 堆石子,数目依次是 7, 6, 5, 47,\ 6,\ 5,\ 4。 每次只能挑相邻的两堆并成一堆,代价是这两堆石子数之和。不断合并,直到剩下唯一一堆——不同的合并顺序,累计代价不同,问最小总代价。

第 0 堆7第 1 堆6第 2 堆5第 3 堆4合并 6+5,代价 11
4 堆石子排成一排,只能合并相邻两堆;合并第 1、2 堆(6 与 5)付出代价 11。

第一反应也许是贪心:每次挑当前最小的相邻两堆并。可这在石子合并里并不总对——因为一堆石子会反复参与后续每一次合并,早并的堆,其石子数会被后面一次次重复计入代价。此刻看着便宜的一步,可能把某堆抬进后续昂贵的合并里。 这是个牵一发动全身的全局问题。

那把「先合哪对、再合哪对」的所有顺序都枚举一遍?nn 堆的合并顺序数量随 nn 指数爆炸,不可行。 但换个角度:无论怎么合,最后一步一定是把某个连续区间的左半与右半两堆并起来。于是问题天然带上了区间的结构——这正是区间 DP 的入口。

状态与转移:枚举最后一次合并的分割点

定状态。设 dp[l][r]dp[l][r] 表示:把第 ll 堆到第 rr 堆这段连续区间合并成一堆所需的最小代价。 要把 [l,r][l,r] 合成一堆,最后一次合并必然是把某个分割点 kk 左边的 [l,k][l,k](已合成一堆)与右边的 [k+1,r][k+1,r](也已合成一堆)并起来。

合并区间 [l, r]dp[l][r] = ?枚举分割点 k:先合出左半,再合出右半左半已合成一堆dp[l][k]右半已合成一堆dp[k+1][r]dp[l][k] + dp[k+1][r] + sum(l..r)
dp[l][r] 枚举分割点 k:左半 dp[l][k] 与右半 dp[k+1][r] 各自先合成一堆,再合并这最后两堆,追加代价 = 整段区间和 sum(l..r)。

这最后一并的代价是多少?两堆分别是 [l,k][l,k] 和 [k+1,r][k+1,r] 的全部石子,加起来恰好是整段区间的石子总和 sum(l,r)\mathrm{sum}(l,r)——与 kk 断在哪里无关。 所以在分割点 kk 处断开的总代价是 dp[l][k]+dp[k+1][r]+sum(l,r)dp[l][k]+dp[k+1][r]+\mathrm{sum}(l,r)。究竟断在哪个 kk 最好?把每个 kk 都试一遍,取最小:

dp[l][r]=min⁡l≤k≤r−1(dp[l][k]+dp[k+1][r])+sum(l,r)dp[l][r]=\min_{l\le k\le r-1}\big(dp[l][k]+dp[k+1][r]\big)+\mathrm{sum}(l,r)

边界:dp[l][l]=0dp[l][l]=0(单独一堆无需合并)。答案:dp[1][n]dp[1][n]。 区间和用前缀和 sum(l,r)=pre[r]−pre[l−1]\mathrm{sum}(l,r)=pre[r]-pre[l-1] 一步取到,不必每次重扫。

这里藏着区间 DP 与线性 DP 的关键分野:dp[l][r]dp[l][r] 依赖的是比它更短的子区间([l,k][l,k] 与 [k+1,r][k+1,r] 长度都 <r−l+1<r-l+1)。所以递推不能按 ll 或 rr 顺序走,必须按区间长度由短到长——短的先算好,长的才有得引用。

本质

区间 DP 把「合并顺序的指数爆炸」压成一张 O(n2)O(n^2) 的三角表:每个连续区间只算一次最优,靠枚举最后一次合并的分割点把大区间拆成两个更短的、已解的子区间。「最后一并的代价与分割点无关,恒为区间和」——正是这一条让转移得以成立。

跟着算一遍

用开头的例子(石子 a=[7,6,5,4]a=[7,6,5,4],下标 1..41..4)走几步。前缀和 pre=[0,7,13,18,22]pre=[0,7,13,18,22],重点盯住长度由短到长:

0
对角线(长度 1)。 每堆单独一堆,无需合并:dp[l][l]=0dp[l][l]=0。这是整张三角表的地基。
1
长度 2:只有一种分法。dp[1][2]=0+0+sum(1,2)=13dp[1][2]=0+0+\mathrm{sum}(1,2)=13;同理 dp[2][3]=11dp[2][3]=11、dp[3][4]=9dp[3][4]=9。
2
长度 3,看 [1,3][1,3](区间和 sum=18\mathrm{sum}=18):k=1k=1 → dp[1][1]+dp[2][3]=0+11=11dp[1][1]+dp[2][3]=0+11=11;k=2k=2 → dp[1][2]+dp[3][3]=13+0=13dp[1][2]+dp[3][3]=13+0=13。取小 1111,加 1818 → dp[1][3]=29dp[1][3]=29。同理 dp[2][4]=9+15=24dp[2][4]=9+15=24。
3
长度 4,看整段 [1,4][1,4](区间和 sum=22\mathrm{sum}=22):k=1k=1 → 0+24=240+24=24;k=2k=2 → 13+9=2213+9=22;k=3k=3 → 29+0=2929+0=29。取小 2222,加 2222 → dp[1][4]=44dp[1][4]=44——正是最小合并代价。
下面的演示会把三角表按长度一层层填满,高亮每个 dp[l][r]dp[l][r] 选中的分割点与它的两个子区间来源。改改石子数,看表实时重算。

看三角表一层一层长出来 · 枚举分割点、区间和相加

一排石子(相邻可合并 · 可改每堆数值 · 3~5 堆)
0
石子数 a
7
1
石子数 a
6
2
石子数 a
5
3
石子数 a
4
r=0
r=1
r=2
r=3
l=0
l=1
l=2
l=3
0
·
·
·
·
0
·
·
·
·
0
·
·
·
·
0
当前计算 依赖来源 被选转移 已确定
dp[l][l]=0dp[l][l] = 0
对角线(区间长度 1):单独一堆石子无需合并,代价为 0——dp[l][l]=0。下三角(l>r)不是合法区间,留作空白。这是整张三角表的地基。
已暂停,第 1 步,共 8 步,1 倍速

为什么按长度递推:填表顺序与复杂度

区间 DP 的表是个上三角(只有 l≤rl\le r 才是合法区间,下三角空着)。转移 dp[l][r]dp[l][r] 要用 dp[l][k]dp[l][k] 与 dp[k+1][r]dp[k+1][r],这两者的区间长度都比 [l,r][l,r] 短。 所以只要先把所有短区间算完,长区间需要的子区间就一定都已就绪——这就是「外层枚举长度 len=2…n\mathrm{len}=2\ldots n,内层枚举左端点 ll」的由来。

r=0r=1r=2r=3l=0l=1l=2l=3len1[0,0]len2[0,1]len3[0,2]len4[0,3]len1[1,1]len2[1,2]len3[1,3]len1[2,2]len2[2,3]len1[3,3]长度递增
三角表沿对角线成层:主对角线是长度 1(已知 0),每向右上错一格长度加 1。填表从对角线出发,一层层推向右上角 dp[1][n]。

数一数计算量:区间长度、左端点合起来约 O(n2)O(n^2) 个区间,每个区间还要枚举分割点 kk(O(n)O(n) 个),于是总复杂度 O(n3)O(n^3)。 对石子合并的常见数据范围(n≤n\le 几百)绰绰有余。三层循环、外层是长度——这是几乎所有区间 DP 的通用骨架,记死它:

for 长度 len = 2 … n:          // ★外层枚举区间长度,由短到长
  for 左端点 l = 1 … n-len+1:
    r = l + len - 1
    for 分割点 k = l … r-1:
      dp[l][r] = min( dp[l][r], dp[l][k] + dp[k+1][r] + sum(l,r) )

一题双问:把 min 换成 max

石子合并的经典题(P1880)常常同时问最小与最大合并代价。好消息是:状态、转移骨架一字不改——只把那个 min⁡\min 换成 max⁡\max,就从「最省」翻成「最费」:

dpmax⁡[l][r]=max⁡l≤k≤r−1(dpmax⁡[l][k]+dpmax⁡[k+1][r])+sum(l,r)dp_{\max}[l][r]=\max_{l\le k\le r-1}\big(dp_{\max}[l][k]+dp_{\max}[k+1][r]\big)+\mathrm{sum}(l,r)

两问共用同一套三层循环,用两张表 ff(最小)、gg(最大)并行填即可。下面把二者并排跑给你看:左边求最小、右边求最大。默认还是 a=[7,6,5,4]a=[7,6,5,4]——最小 4444、最大 5353。改改石子数,看两个答案与各自选中的分割点如何分道扬镳。

同一排石子(两侧共用 · 可改数值 / 增删堆)
0
石子数 a
7
1
石子数 a
6
2
石子数 a
5
3
石子数 a
4
最小合并代价 dp[0][3] = 44 · 最大合并代价 dp[0][3] = 53 · 同一组石子、同一套转移,只把 opt 从 min 换成 max,两问差 9。
最小合并代价(opt = min)
r=0
r=1
r=2
r=3
l=0
l=1
l=2
l=3
0
·
·
·
·
0
·
·
·
·
0
·
·
·
·
0
当前计算 依赖来源 被选转移 已确定
dp[l][l]=0dp[l][l] = 0
对角线(区间长度 1):单独一堆石子无需合并,代价为 0——dp[l][l]=0。下三角(l>r)不是合法区间,留作空白。这是整张三角表的地基。
已暂停,第 1 步,共 8 步,1 倍速
最大合并代价(opt = max)
r=0
r=1
r=2
r=3
l=0
l=1
l=2
l=3
0
·
·
·
·
0
·
·
·
·
0
·
·
·
·
0
当前计算 依赖来源 被选转移 已确定
dp[l][l]=0dp[l][l] = 0
对角线(区间长度 1):单独一堆石子无需合并,代价为 0——dp[l][l]=0。下三角(l>r)不是合法区间,留作空白。这是整张三角表的地基。
已暂停,第 1 步,共 8 步,1 倍速

别忘了:这排石子其实是环

P1880 原题里,石子摆成一个环——第 nn 堆与第 11 堆也相邻。本页先把它当作链讲透区间 DP 的内核;处理「环」的通法(断环为链:复制一倍接成 2n2n 长,枚举所有长度为 nn 的窗口)留到 环形区间 DP 一节专门拆解。链形基底是环形的地基。

例题

P1880[NOI1995] 石子合并NOI1995提高+/省选-
题意
nn 堆石子摆成一环,每次合并相邻两堆、代价为两堆之和,直到并成一堆。分别求最小与最大总代价。
对应关系
标准区间 DP:dp[l][r]dp[l][r] = 合并 [l,r][l,r] 的最优代价,枚举分割点 kk,追加代价 = 区间和。本页的链形 + 一题双问就是它的内核。
环的处理(下一节展开)
断环为链:把石子数组复制一倍拼成长度 2n2n,在其上做链形区间 DP,再取所有长度为 nn 的区间 [i,i+n−1][i,i+n-1] 里的最优。参考代码先给链形双问骨架(把 nn 换成 2n2n 并加一层窗口枚举即得环形)。
转移 · 复杂度
f/g[l][r]=opt(f/g[l][k]+f/g[k+1][r])+sum(l,r)f/g[l][r]=\mathrm{opt}(f/g[l][k]+f/g[k+1][r])+\mathrm{sum}(l,r),外层长度、内层左端点、最内分割点;时间 O(n3)O(n^3)。
参考代码(链形基底 · 双问并行)
#include <iostream>
#include <cstring>
using namespace std;

const int INF = 0x3f3f3f3f;
int a[105];                   // 链形基底:n 堆石子(环形拆解留到下一节)
int pre[105];                 // 前缀和,sum(l..r) = pre[r] - pre[l-1]
int f[105][105];             // 最小合并代价
int g[105][105];             // 最大合并代价

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        pre[i] = pre[i - 1] + a[i];
    }

    for (int len = 2; len <= n; len++)          // ★外层枚举区间长度,由短到长
        for (int l = 1; l + len - 1 <= n; l++)
        {
            int r = l + len - 1;
            int s = pre[r] - pre[l - 1];        // 本区间合并的代价(区间和)
            f[l][r] = INF;
            g[l][r] = -INF;
            for (int k = l; k <= r - 1; k++)    // 枚举分割点 k
            {
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s);
                g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s);
            }
        }

    cout << f[1][n] << endl;                     // 一题双问:最小、最大
    cout << g[1][n] << endl;
    return 0;
}
P5019[NOIP2018 提高组] 铺设道路NOIP2018 提高组普及/提高-
题意
一排 nn 段道路,第 ii 段深度 did_i。每天可把一段连续区间的深度整体填平 1。求填平所有段的最少天数。
为什么选它
较新的 CSP/NOIP 真题,是理解「区间合并代价」直觉的极佳前菜:把「填平连续区间」这一操作,和石子合并里「合并连续区间」并置——都在连续段上思考代价。它的最优解可由差分一眼看穿:只有当前段比前一段更深时才需新增 di−di−1d_i-d_{i-1} 天,累加即答案 ∑max⁡(0, di−di−1)\sum\max(0,\ d_i-d_{i-1})——一个 O(n)O(n) 的贪心/差分,正好反衬石子合并为何非 DP 不可。
参考代码(差分累加)
#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
    long long ans = 0;
    int prev = 0;                                // 上一格的高度(差分视角)
    for (int i = 1; i <= n; i++)
    {
        int h;
        cin >> h;
        if (h > prev)                            // 只在“抬高”处付出铺设次数
            ans += h - prev;
        prev = h;
    }
    cout << ans << endl;
    return 0;
}

练习

P1775石子合并(弱化版)纯链形石子合并模板:石子排成一条链(非环)。前缀和求区间代价,外层枚举长度、内层左端点、最内分割点,dp[1][n] 即答案。把三层循环骨架默写下来。在洛谷打开
P1043[NOIP2003 普及组] 数字游戏环形 + 区间 DP:环上分 m 段,各段和对 10 取模后相乘,求最大/最小。断环为链(复制一倍)后,dp[l][r][k] 记「区间 [l,r] 分成 k 段」的最优,转移枚举最后一段的分割点;取模后可能为负,最小值转移别漏负负得正。在洛谷打开
想更直观地感受「合并顺序如何改变总代价」?到 C 部分页的互动里亲手挑一次合并顺序,再看 DP 给出的最优。

已进入 石子合并(链形) · 区间 DP · DP大师