平方代价分段 DP 的斜率优化:把转移来源看成点 (S[j], dp[j]+S[j]²),单调队列维护下凸壳,队头即最优决策,O(N) 完成转移。
OJ: roj
题目 ID: 1613
难度:提高+/省选-
标签:斜率优化动态规划单调队列凸壳
创建: 2026-09-30 21:52
更新: 2026-10-07 13:50
形式化题目
给定非负整数序列
求一种切分方式,使所有段的费用之和最小,输出这个最小值。多组数据,读到文件末尾为止。
正解
思路
第一步:分段 DP。 设
这有
第二步:把「选来源」变成「选直线」。 展开平方项并整理:
把每个决策
第三步:两个单调性引出单调队列。 最小截距只会在这个点集的下凸壳上取到。而本题恰好有两个天然的单调性:
非负 随 递增,点按 从小到大依次产生,天然按 排好序; 递增 询问斜率 只增不减。
于是用单调队列维护下凸壳:
- 队头弹出:若队头相邻两点的斜率
,说明切点已经在更右边,队头以后永远不会再优,弹出; - 队尾弹出:新点
入队前,若 ,则 落在上凸的位置上,不可能再成为任何询问的最小截距点,弹出。
每个下标至多进出队列一次,转移总复杂度
斜率怎么比? 全部用交叉相乘:long long。
一个坑:
边界:
样例推演(
| 处理后队列中的下标 | 队头 |
||
|---|---|---|---|
| 1 | 0, 1 | 0 | |
| 2 | 0, 1, 2 | 1 | |
| 3 | 0, 2, 3 | 2 | |
| 4 | 0, 2, 3, 4 | 3 | |
| 5 | 0, 2, 4, 5 | 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 22:10
# update_at: 2026-09-30 22:10
import sys
# 凸包点 j 的纵坐标:dp[j] + S[j]^2(横坐标是 S[j],斜率优化推式子里移项得到)
def y_of(dp: list[int], S: list[int], j: int) -> int:
"""直线截距式 y(j) = dp[j] + S[j]²,凸包上点 j 的 y 值。"""
return dp[j] + S[j] * S[j]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
while True: # 多组数据,读到 EOF
try:
n = next(data)
except StopIteration:
break
m = next(data)
if n == 0: # 没有单词,不用打印,费用为 0
out.append('0')
continue
S = [0] * (n + 1) # S[i] = 前 i 个单词的权值和
s = 0
for i in range(1, n + 1):
s += next(data)
S[i] = s
dp = [0] * (n + 1) # dp[i]:前 i 个单词分段的最小费用
q = [0] * (n + 1) # 凸包队列,存下标,X 坐标 S[j] 严格递增
head, tail = 0, 1 # 队列有效区间 [head, tail)
for i in range(1, n + 1):
k = 2 * S[i] # 当前决策直线的斜率,随 i 单调不减 → 可从队头淘汰
# 队头两点斜率 <= k 时,队头不可能再成为最优决策
while tail - head >= 2 and \
(y_of(dp, S, q[head + 1]) - y_of(dp, S, q[head])) <= \
k * (S[q[head + 1]] - S[q[head]]):
head += 1
j = q[head]
d = S[i] - S[j]
dp[i] = dp[j] + d * d + m
# 新点加入队尾前,先弹出使凸包失去"斜率严格递增"的点
while tail - head >= 2 and \
(y_of(dp, S, q[tail - 1]) - y_of(dp, S, q[tail - 2])) * (S[i] - S[q[tail - 2]]) >= \
(y_of(dp, S, i) - y_of(dp, S, q[tail - 2])) * (S[q[tail - 1]] - S[q[tail - 2]]):
tail -= 1
if S[i] > S[q[tail - 1]]:
q[tail] = i
tail += 1
# S[i] == S[q[-1]] 时 dp 非降 ⇒ 新点 y 更大,被旧点完全覆盖,不入队
out.append(str(dp[n]))
print('\n'.join(out))
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-05 12:55
* update_at: 2026-10-05 12:55
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 500005;
// 注意值域:N <= 5*10^5,C 为非负权值,前缀和、dp 均在 ll 范围内
ll C[MAXN]; // 每个单词的权值
ll S[MAXN]; // S[i] = 前 i 个单词的权值和
ll dp[MAXN]; // dp[i]:前 i 个单词分段打印的最小费用
int q[MAXN]; // 单调队列:保存凸壳上的决策下标,x 坐标 S[j] 严格递增
int head, tail; // 队列有效区间 [head, tail)
// 凸壳上点 j 的纵坐标:由转移式移项得到 y(j) = dp[j] + S[j]^2
inline ll y_of(int j) {
return dp[j] + S[j] * S[j];
}
int main() {
int n;
ll m;
// 多组数据,读到文件末尾为止
while (scanf("%d %lld", &n, &m) == 2) {
if (n == 0) { // 一个单词都不用打印,费用为 0
printf("0\n");
continue;
}
for (int i = 1; i <= n; ++i) {
scanf("%lld", &C[i]);
S[i] = S[i - 1] + C[i];
}
// 决策下标 0(一个单词都不选)先入队
head = 1; tail = 1;
q[tail++] = 0;
for (int i = 1; i <= n; ++i) {
ll k = 2 * S[i]; // 当前决策直线的斜率,随 i 单调不减
// 队头:相邻两点斜率 <= k 时,队头不再可能是最优决策,弹出
while (tail - head >= 2 &&
(y_of(q[head + 1]) - y_of(q[head])) <= k * (S[q[head + 1]] - S[q[head]])) {
++head;
}
// 队头即最优决策
int j = q[head];
ll d = S[i] - S[j];
dp[i] = dp[j] + d * d + m;
// 队尾:弹出破坏下凸壳(斜率不严格递增)的点
// 斜率比较全部用交叉相乘,避免除法和除零
while (tail - head >= 2 &&
(y_of(q[tail - 1]) - y_of(q[tail - 2])) * (S[i] - S[q[tail - 2]]) >=
(y_of(i) - y_of(q[tail - 2])) * (S[q[tail - 1]] - S[q[tail - 2]])) {
--tail;
}
// C_i = 0 会导致 S[i] = S[q[tail-1]](x 重复):
// 此时 dp 非降,新点 y 更大,被旧点完全覆盖,直接不入队
if (S[i] > S[q[tail - 1]]) {
q[tail++] = i;
}
}
printf("%lld\n", dp[n]);
}
return 0;
}复杂度
每组数据:队列中每个下标至多入队、出队各一次,队头/队尾操作均摊
图示解析
凸壳与切点:为什么队头就是最优决策
下图展示样例推演到
y
│ ● P4(26, 876) ← 队头即切点:slope(P3,P4)≈52.7 ≤ 62 ≤ slope(P4,P5)=63
│ ● P3(19, 507)
│ ● P2(14, 312)
│ ● P0(0, 0)
└──────────────────────────────────── x = S[j]
P0 P2 P3 P4 P5(31,1191)读图要点:
- 询问直线是一族斜率为
的平行线,从下往上平移,第一个碰到的点就是最小截距点(切点); - 切点左侧相邻边的斜率一定
、右侧相邻边的斜率一定 ; 只增,所以切点只会右移,被淘汰的队头不必保留; - 队尾弹出的对象是「上凸点」:如
在 与 连线上方,任何直线扫到 时都先碰到 或 ,它永远不优。
总结
- 本题是斜率优化的入门经典:平方代价分段 DP + 决策单调。
- 推导套路:把
里的 展开,将转移改写成「定斜率直线扫点取最小截距」; 、斜率两个单调性成立时,用单调队列维护下凸壳即可 。 - 实现要点:斜率比较用交叉相乘防溢出/防除零;
导致的 重复点不入队; 输出 ;多组数据逐组重建。