环形区间 DP:把石子序列复制一份接到末尾,枚举长度 n 的窗口对应每种断环方式,f[i][j]/g[i][j] 分别表示区间 [i,j] 合并成一堆的最小/最大得分。
OJ: roj
题目 ID: 1569
难度:普及+/提高-
标签:动态规划python
创建: 2026-09-30 19:11
更新: 2026-10-07 13:50
形式化题目
有
正解
思路
先看最朴素的思路:每一步枚举所有相邻的有序对合并,递归
关键观察:把视角从"每一步"换到"某一段"。 任意一个合法的合并方案,都等价于不断把相邻的若干堆并成一段,最后整段并成一堆。也就是说,方案可以组织成"把区间
再往下一步:假设区间
——它只和区间边界有关,和断点
以样例 4 5 9 4 为例,
| 区间长度 | |||||
|---|---|---|---|---|---|
注意长度
环怎么处理? 上面推的是链。环形方案的特点是:只要最终合并顺序不跨越某条边,它就和一条链上的方案一一对应。做法是把序列复制一份接到末尾,得到长度
递推方向由区间长度决定:长度
取最小值时需要哨兵:用
代码
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 19:25
# update_at: 2026-09-30 19:25
import sys
INF = 10 ** 9 # 最小值表的哨兵:比任何合法得分(总和 <= 200*10^9)都小的上界由 BIG 保证
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
stone = [next(data) for _ in range(n)]
BIG = sum(stone) * n + 1 # 任何方案得分都 <= n * 总和,用它当正无穷
# 环形 → 链形:把序列接一份在后面,长度为 m 的区间就覆盖了任意环形起点的 m 堆
a = stone * 2
m = 2 * n # 实际只用前 m 个下标;区间 [i, j] 表示第 i..j 堆合并成一堆
# pre[k] = 前 k 堆的和,区间得分用 pre[j+1]-pre[i] 一次减出来
pre = [0] * (m + 1)
for k in range(m):
pre[k + 1] = pre[k] + a[k]
# f[i][j] / g[i][j]:把 [i, j] 合并成一堆的最小 / 最大得分
f = [[0] * m for _ in range(m)]
g = [[0] * m for _ in range(m)]
for length in range(2, n + 1): # 合并 length 堆;length=1 的区间本身不用花钱
for i in range(m - length + 1):
j = i + length - 1
total = pre[j + 1] - pre[i] # 最后一次合并必得的分:两段之和就是整段和
best_min, best_max = BIG, -1
for k in range(i, j): # 枚举断点:左段 [i,k] 与右段 [k+1,j] 先各自成堆
left_min, left_max = f[i][k], g[i][k]
right_min, right_max = f[k + 1][j], g[k + 1][j]
s_min = left_min + right_min
s_max = left_max + right_max
if s_min < best_min:
best_min = s_min
if s_max > best_max:
best_max = s_max
f[i][j] = best_min + total
g[i][j] = best_max + total
# 拆环的每个位置 r 都是候选起点,起点固定的链长 n 区间 [r, r+n-1] 就是该方案
print(min(f[r][r + n - 1] for r in range(n)))
print(max(g[r][r + n - 1] for r in range(n)))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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 22:20
* update_at: 2026-10-04 22:20
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 405; // 2n + 5,环拆成链后长度翻倍
typedef long long ll;
ll n, m; // n 堆石子;m = 2n 为拆环成链后的总长
ll a[MAXN]; // 拆环后的石子序列:原序列接一份在后面
ll pre[MAXN]; // pre[k] = 前 k 堆石子之和,O(1) 取区间和
ll f[MAXN][MAXN]; // f[i][j]:把区间 [i,j] 合并成一堆的最小得分
ll g[MAXN][MAXN]; // g[i][j]:把区间 [i,j] 合并成一堆的最大得分
ll BIG; // 最小值表的哨兵,任何合法得分都达不到
// 读入并把环拆成两倍长的链。
void read_input() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
}
for (ll i = 1; i <= n; i++) {
a[i + n] = a[i]; // 复制一份接到末尾,破环成链
}
m = 2 * n;
}
void solve() {
// 前缀和:sum(i,j) = pre[j] - pre[i-1]
pre[0] = 0;
for (ll i = 1; i <= m; i++) {
pre[i] = pre[i - 1] + a[i];
}
// 哨兵:任何方案得分不超过 n 倍石子总和(每次得分 <= 总和,共 n-1 次)
BIG = pre[n] * n + 1;
// 长度 1 的区间 f = g = 0(全局数组默认即 0)
// 按区间长度从小到大递推:长度 L 只依赖更短的子区间
for (ll len = 2; len <= n; len++) {
for (ll i = 1; i + len - 1 <= m; i++) {
ll j = i + len - 1;
ll sum_ij = pre[j] - pre[i - 1]; // 最后一次合并必得整段和,与断点无关
ll best_min = BIG;
ll best_max = -1;
// 枚举断点:左段 [i,k]、右段 [k+1,j] 先各自合成一堆
for (ll k = i; k < j; k++) {
ll s_min = f[i][k] + f[k + 1][j];
ll s_max = g[i][k] + g[k + 1][j];
if (s_min < best_min) best_min = s_min;
if (s_max > best_max) best_max = s_max;
}
f[i][j] = best_min + sum_ij;
g[i][j] = best_max + sum_ij;
}
}
// 每个长度为 n 的窗口 [r, r+n-1] 对应一种断环方式,枚举所有起点取最值
ll ans_min = BIG;
ll ans_max = -1;
for (ll r = 1; r <= n; r++) {
if (f[r][r + n - 1] < ans_min) ans_min = f[r][r + n - 1];
if (g[r][r + n - 1] > ans_max) ans_max = g[r][r + n - 1];
}
cout << ans_min << "\n";
cout << ans_max << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:
。状态数 (在 上有 个区间),每个状态枚举 个断点, 时约为 次转移。 - 空间复杂度:
,即两张 的 DP 表。
总结
- 把"枚举操作序列"改写成"枚举区间"是本题的核心一步:最后一次合并的得分只由区间和决定,与断点无关,最优化因此可以下推到子区间。
- 环形问题用"复制一份接到末尾 + 枚举长度
的窗口"转成 条链,是环形 DP 的通用技巧。 - 与之同名但要求环形、且合并后仍相邻的最优三角剖分类问题(如"环形石子合并 + 凸多边形三角剖分"),也用同一套"复制成链 + 区间 DP"框架。