石子合并(链形)
区间合并基础模型
本课摘要
石子合并(链形)课程回答“区间合并代价为何要按长度递增计算”。内容以区间合并基础模型为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断石子合并(链形)的适用条件与状态边界
- 围绕“区间合并基础模型”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
把相邻的石子并成一堆
先看一个具体场景:一排摆着 4 堆石子,数目依次是 。 每次只能挑相邻的两堆并成一堆,代价是这两堆石子数之和。不断合并,直到剩下唯一一堆——不同的合并顺序,累计代价不同,问最小总代价。
第一反应也许是贪心:每次挑当前最小的相邻两堆并。可这在石子合并里并不总对——因为一堆石子会反复参与后续每一次合并,早并的堆,其石子数会被后面一次次重复计入代价。此刻看着便宜的一步,可能把某堆抬进后续昂贵的合并里。 这是个牵一发动全身的全局问题。
那把「先合哪对、再合哪对」的所有顺序都枚举一遍? 堆的合并顺序数量随 指数爆炸,不可行。 但换个角度:无论怎么合,最后一步一定是把某个连续区间的左半与右半两堆并起来。于是问题天然带上了区间的结构——这正是区间 DP 的入口。
状态与转移:枚举最后一次合并的分割点
定状态。设 表示:把第 堆到第 堆这段连续区间合并成一堆所需的最小代价。 要把 合成一堆,最后一次合并必然是把某个分割点 左边的 (已合成一堆)与右边的 (也已合成一堆)并起来。
这最后一并的代价是多少?两堆分别是 和 的全部石子,加起来恰好是整段区间的石子总和 ——与 断在哪里无关。 所以在分割点 处断开的总代价是 。究竟断在哪个 最好?把每个 都试一遍,取最小:
边界:(单独一堆无需合并)。答案:。 区间和用前缀和 一步取到,不必每次重扫。
这里藏着区间 DP 与线性 DP 的关键分野: 依赖的是比它更短的子区间( 与 长度都 )。所以递推不能按 或 顺序走,必须按区间长度由短到长——短的先算好,长的才有得引用。
本质
区间 DP 把「合并顺序的指数爆炸」压成一张 的三角表:每个连续区间只算一次最优,靠枚举最后一次合并的分割点把大区间拆成两个更短的、已解的子区间。「最后一并的代价与分割点无关,恒为区间和」——正是这一条让转移得以成立。
跟着算一遍
用开头的例子(石子 ,下标 )走几步。前缀和 ,重点盯住长度由短到长:
看三角表一层一层长出来 · 枚举分割点、区间和相加
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
为什么按长度递推:填表顺序与复杂度
区间 DP 的表是个上三角(只有 才是合法区间,下三角空着)。转移 要用 与 ,这两者的区间长度都比 短。 所以只要先把所有短区间算完,长区间需要的子区间就一定都已就绪——这就是「外层枚举长度 ,内层枚举左端点 」的由来。
数一数计算量:区间长度、左端点合起来约 个区间,每个区间还要枚举分割点 ( 个),于是总复杂度 。 对石子合并的常见数据范围( 几百)绰绰有余。三层循环、外层是长度——这是几乎所有区间 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)常常同时问最小与最大合并代价。好消息是:状态、转移骨架一字不改——只把那个 换成 ,就从「最省」翻成「最费」:
两问共用同一套三层循环,用两张表 (最小)、(最大)并行填即可。下面把二者并排跑给你看:左边求最小、右边求最大。默认还是 ——最小 、最大 。改改石子数,看两个答案与各自选中的分割点如何分道扬镳。
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
别忘了:这排石子其实是环
P1880 原题里,石子摆成一个环——第 堆与第 堆也相邻。本页先把它当作链讲透区间 DP 的内核;处理「环」的通法(断环为链:复制一倍接成 长,枚举所有长度为 的窗口)留到 环形区间 DP 一节专门拆解。链形基底是环形的地基。
例题
#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;
}#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;
}
