区间 DP:把石子排列看作连续区间,dp[l][r] 表示合并区间 [l,r] 的最小得分,枚举最后一次合并位置转移。
OJ: roj
题目 ID: 1274
难度:普及+/提高-
标签:动态规划区间DP
创建: 2026-09-30 02:51
更新: 2026-10-07 11:01
形式化题目
给定一个长度为
正解
思路
如果直接枚举所有合并顺序,合法顺序的数量是卡特兰数
设
其中
样例 DP 表
下面以样例 13 7 8 16 21 4 18 展示部分 DP 表(
| l\r | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 20 | 43 | 87 | 145 | 178 | 239 |
| 2 | 0 | 15 | 46 | 98 | 127 | 185 | |
| 3 | 0 | 24 | 69 | 98 | 156 | ||
| 4 | 0 | 37 | 66 | 118 | |||
| 5 | 0 | 25 | 65 | ||||
| 6 | 0 | 22 | |||||
| 7 | 0 |
表中第 1 行第 7 列的
代码
Python 版:
python
#!/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-09-30 02:51
# update_at: 2026-09-30 02:51
import sys
from functools import cache
INF = 10 ** 18 # 比任何可能答案都大
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
a = [next(data) for _ in range(n)]
# 前缀和:s[i] 表示前 i 堆石子总数,s[0] = 0。
s = [0] * (n + 1)
for i, x in enumerate(a, 1):
s[i] = s[i - 1] + x
@cache
def dfs(l: int, r: int) -> int:
"""把第 l 到第 r 堆石子合并成一堆的最小得分,区间为 1-based 闭区间。"""
if l == r:
return 0
return min(dfs(l, k) + dfs(k + 1, r) for k in range(l, r)) + s[r] - s[l - 1]
print(dfs(1, n))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* 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-04 23:42
* update_at: 2026-10-04 23:42
*/
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 105;
const ll INF = 1000000000000000000LL;
ll n;
ll a[MAXN]; // a[i] 表示第 i 堆石子的数量
ll s[MAXN]; // s[i] = 前 i 堆石子的总数(前缀和),用于 O(1) 求区间和
ll dp[MAXN][MAXN]; // dp[l][r] 表示把第 l 到第 r 堆石子合并成一堆的最小得分
void read_input() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
}
void solve() {
// 按区间长度从小到大递推,计算 dp[l][r] 时它依赖的子区间都已经算好。
// 长度为 1 的区间不需要合并,dp[l][l] 由全局数组默认为 0。
for (ll len = 2; len <= n; len++) {
for (ll l = 1; l + len - 1 <= n; l++) {
ll r = l + len - 1;
dp[l][r] = INF;
// 枚举最后一次合并的分割点 k,把区间 [l, r] 分成 [l, k] 与 [k+1, r] 两堆。
for (ll k = l; k < r; k++) {
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
// 最后一次合并的得分就是整个区间的石子总数。
dp[l][r] += s[r] - s[l - 1];
}
}
cout << dp[1][n] << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 状态数:
。 - 每个状态枚举分割点
: 。 - 时间复杂度:
。当 时约为 次运算。 - 空间复杂度:
,用于 DP 表和前缀和数组。
总结
合并石子是一道经典的区间 DP 模板题。核心观察是:任何合并过程的中间状态都对应原序列的一个连续子区间,因此可以用