把枚举合并顺序换成枚举最后一次合并的断点,得到区间 DP:f(l,r)=min(f(l,k)+f(k+1,r))+S(l,r),按区间长度递推,O(N^3) 时间、O(N^2) 空间。
OJ: roj
题目 ID: 3145
难度:普及+/提高-
标签:动态规划区间DP前缀和python
创建: 2026-10-01 20:33
更新: 2026-10-10 11:30
形式化题目
给定长度为
求把所有元素合并成一段所需的最小总代价。
等价说法:每次把相邻的两个连续区间并为一个大区间,代价为合并后区间的元素之和;求把
暴力解法
思路
最直白的做法就是照着题意模拟:维护当前局面(若干堆石子排成一排),每一步枚举所有相邻的两堆并合并,递归下去直到只剩一堆,用总代价更新答案。
样例
| 合并顺序 | 各步代价 | 总代价 |
|---|---|---|
| 22 | ||
| 22 | ||
| 24 | ||
| 28 | ||
| 28 | ||
| 29 |
最小值 22 正是样例输出。表里还藏着两条线索:第 1、2 行只是交换了两段内部的先后顺序,总代价完全一样;而换了断点之后,代价从 22 一路涨到 29。
代码
本层只用文字与表格说明朴素做法,不单独给出 brute.cpp:它与正解共用同一套“合并树”语义,差别只在枚举的对象是操作序列还是断点,单独放一份代码文件不会带来新信息。上面那张全顺序表已经能完整复现这一层的全部行为。
复杂度
第
| 3 | 4 | 8 | 12 | 300 | |
|---|---|---|---|---|---|
| 合法合并顺序数 |
2 | 6 | 5040 |
看到
瓶颈
暴力枚举的对象是操作序列,而同一棵合并树可以被写成很多个不同的操作序列(表里前两行就是),这些重复正是浪费的来源;同时序列总数按阶乘增长,
但表里也有一个决定性的观察:真正决定总代价的是每次合并在哪里断开,也就是最终那棵合并树长什么样,而不是“左右两段谁先合”。
正解
思路
第一步:只看最后一次合并。
把
[1 . . . . . . . . . . . . N] 代价 += S(1,N)(全段质量之和)
/ \
[1 .. k] [k+1 .. N]
递归求解 递归求解这一步的代价是全部质量之和
枚举断点
第二步:把子问题编号成区间。
式子里的子问题都形如「把
第三步:按区间长度递推。
样例 main.py 的 0 基:— 表示该长度下没有这个起点):
| 区间长度 |
||||
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | |
| 4 | 8 | 7 | — | |
| 13 | 17 | — | — | |
| 22 | — | — | — |
正确性:上界——任取断点
代码
ps 是前缀和,区间和写成 ps[j+1] - ps[i];f[i][j] 就是 best 收集断点
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 石子合并:区间 DP,按区间长度从小到大递推。
#include <cstdio>
#include <vector>
typedef long long ll;
const ll MAXN = 1005;
ll n;
ll stone[MAXN];
ll ps[MAXN]; // ps[i] = 前 i 堆的质量和
ll f[MAXN][MAXN]; // f[i][j]:把区间 [i, j] 合并成一堆的最小代价
int main() {
if (scanf("%lld", &n) != 1) return 0; // 空输入安全返回
for (ll i = 0; i < n; i++) scanf("%lld", &stone[i]);
ps[0] = 0;
for (ll i = 0; i < n; i++) ps[i + 1] = ps[i] + stone[i];
for (ll length = 2; length <= n; length++) {
for (ll i = 0; i + length - 1 < n; i++) {
ll j = i + length - 1;
ll best = -1; // 只作内层取最小值的初始上界
for (ll k = i; k < j; k++) {
ll cost = f[i][k] + f[k + 1][j]; // 左段代价 + 右段代价
if (best < 0 || cost < best) best = cost;
}
f[i][j] = best + ps[j + 1] - ps[i]; // 补上最后一次合并自身的代价
}
}
printf("%lld\n", f[0][n - 1]); // n = 1 时就是 0:只有一堆
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 20:35
# update_at: 2026-10-01 20:49
import sys
from itertools import accumulate
INF = 1 << 60 # 只作内层取最小值的初始上界;真实代价不超过 n * 1000 * (n-1) ≈ 9e7
def min_merge_cost(stone: list[int]) -> int:
"""链上石子合并的最小总代价:区间 DP,O(n^3) 时间、O(n^2) 空间。
f[i][j] = 把区间 [i, j] 合并成一堆的最小代价。最后一次合并必然把区间劈成
左段 [i, k] 与右段 [k+1, j],两段的代价 f[i][k] + f[k+1][j] 之外,
这一次合并自身还要付 S(i, j) = ps[j+1] - ps[i],也就是这段的质量和。
"""
n = len(stone)
ps = list(accumulate(stone, initial=0)) # ps[i] = 前 i 堆的质量和
f = [[0] * n for _ in range(n)] # 初始全 0,正好就是长度 1 的区间(不必合并)
for length in range(2, n + 1):
# f[i][j] 只依赖更短的区间,所以按长度从小到大递推即可保证用到的值已经算好。
for i in range(n - length + 1):
j = i + length - 1
row = f[i] # 内层是热路径,把行取到局部变量,省掉每次的 f[i] 下标查找
best = INF
for k in range(i, j):
cost = row[k] + f[k + 1][j] # 左段代价 + 右段代价,k 是最后一次合并的断点
if cost < best:
best = cost
row[j] = best + ps[j + 1] - ps[i] # 补上最后一次合并自身的代价
return f[0][n - 1] # n = 1 时就是 f[0][0] = 0:只有一堆,不需要任何合并
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
stone = [next(data) for _ in range(n)]
print(min_merge_cost(stone))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:状态数
,每个状态枚举 个断点,合计 。精确计数为 ; 时是 次内层转移,实测最慢用例 0.17 s(时限 1000 ms)。 - 空间复杂度:
个状态加一个长度 的前缀和数组, 时约 个整数;实测峰值内存约 17 MB(限制 128 MB)。
总结
本题的关键不是“用 DP”,而是换一个枚举对象:暴力的对象是操作序列,同一棵合并树会被写成许多种先后顺序,规模按
图示解析
最后一次合并的劈分
下图展示区间
区间 [1, 2, 3, 4](对应 0 基的 [0, 3])的最后一次合并必然劈成左右两个连续子区间:
[1 . . . . . . . . . . . . n] 代价 += S(1,n)(全段质量之和)
/ \
[1 .. 2] [3 .. 4]
递归 f(1,2)=4 递归 f(3,4)=7
样例 [1, 3, 5, 2]:最优断点在第 2 堆之后,S(1,4) = 11,
于是 4 + 7 + 11 = 22。观察重点:转移式里的
从暴力到正解的路线
下图是从枚举合并顺序到区间 DP 的主线,只保留模型、关键观察、算法与答案。
输入 a[1..n],每次合并相邻两堆
|
v
暴力:枚举每一步合并哪两堆 搜索树规模 (n-1)!,n=300 不可能跑完
|
| 同一棵合并树的多种先后顺序被重复枚举 —— 只要看合并树的形状
v
关键观察:最后一次合并把 [l,r] 劈成 [l,k] 与 [k+1,r],代价 S(l,r) 与 k 无关
|
v
区间 DP:f(l,r) = min_k ( f(l,k) + f(k+1,r) ) + S(l,r)
|
| 依赖的区间都更短 -> 按区间长度 L = 2..n 递推
| S(l,r) 用前缀和 O(1) 取得
v
答案 f(0,n-1) O(n^3) 时间、O(n^2) 空间观察重点:暴力与正解的差别不在“枚举的范围”,而在“枚举的对象”——暴力枚举的是操作序列(有