猫只依赖出发下限 a=T-s[H],排序后每位饲养员接一段连续的猫;DP 转移整理成直线族,双单调性下用单调队列维护下凸壳,O(P·M) 求最小等待和。
OJ: roj
题目 ID: 1609
难度:提高+/省选-
标签:动态规划斜率优化单调队列前缀和python一本通
创建: 2026-09-30 21:39
更新: 2026-10-07 13:50
形式化题目
一条路旁有
猫能否被某位饲养员接走,只取决于他的出发时刻
样例
| 猫(按 |
接它的饲养员 | 等待时间 | ||||
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | 0 | 甲( |
0 |
| 2 | 2 | 1 | 1 | 0 | 甲 | 0 |
| 3 | 4 | 9 | 9 | 0 | 甲 | 0 |
| 6 | 3 | 12 | 4 | 8 | 乙( |
2 |
| 5 | 2 | 10 | 1 | 9 | 乙 | 1 |
| 4 | 1 | 10 | 0 | 10 | 乙 | 0 |
注意两点:甲
正解
思路
第一步:排序后,每位饲养员接走一段连续的猫。 饲养员走完全程,凡是
第二步:每段的最优出发时刻是段尾的
其中
第三步:分段 DP 与瓶颈。 设
第四步:把转移整理成直线族。 把只与
每个切点
- 斜率单调:插入顺序
,斜率 严格递减; - 查询单调:
递增时查询点 非降(已排序)。
斜率单调、查询单调 ⇒ 用双端队列维护直线族的下凸壳:查询时若 value(front) >= value(second) 就弹队首(首线被追平后对更大的
样例的两层 DP。 下表按
| 0 | — | 0 | 0 | 0 | — |
| 1 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 | 0 |
| 4 | 8 | 8 | 24 | 0 | 3 |
| 5 | 9 | 17 | 28 | 1 | 3 |
| 6 | 10 | 27 | 33 | 3 | 3 |
表中能读出三件事:一人全接(
代码
下面的 dp_layer() 就是上述单调队列:dq 存直线 while 用交叉相乘判死线,查询 while 弹队首,f[r] = r*x - pre[r] + 最优直线取值 即第四步整理出的三段;循环里先插 solve() 负责读入、算
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 21:45
# update_at: 2026-09-30 21:45
import sys
from collections import deque
from itertools import accumulate
INF = 10**30 # 不可达 DP 值的哨兵,远大于任何合法答案
def dp_layer(prev: list[int], limit: list[int], pre: list[int]) -> list[int]:
"""多用一只饲养员推进一层 DP:f[r] = min_k prev[k] + (r-k)*a_r - (pre[r]-pre[k])。
猫按 a 升序排好后,每只饲养员接走的必是一段连续的猫,段 (k, r] 的最优出发时刻
就是段尾 a_r;把转移整理成 f[r] = r*a_r - pre[r] + min_k {(prev[k]+pre[k]) - k*a_r},
每个 k 就是一条斜率 -k、截距 prev[k]+pre[k] 的直线。加入序斜率递减、查询点
a_r 递增(非降),用单调队列维护下凸壳,摊还每步 O(1)。
"""
dq: deque[tuple[int, int]] = deque() # (斜率, 截距),斜率自队头向队尾递减
n = len(limit)
f = [INF] * (n + 1)
for r in range(n + 1):
if prev[r] < INF: # 不可达的切点 k 不入壳
m, b = -r, prev[r] + pre[r]
# 队尾直线的生效区间被新交点截空(x12 <= x23)→ 它永远取不到最小值
while len(dq) >= 2 and (b - dq[-1][1]) * (dq[-2][0] - dq[-1][0]) <= (dq[-1][1] - dq[-2][1]) * (dq[-1][0] - m):
dq.pop()
dq.append((m, b))
if r == 0:
f[0] = 0 # 0 只猫的费用恒为 0,它也是下一层 k=0 的直线来源
continue
x = limit[r - 1] # a_r:段尾猫要求饲养员不早于它出发
# 查询点递增,队头若已不如第二条就出队
while len(dq) >= 2 and dq[0][0] * x + dq[0][1] >= dq[1][0] * x + dq[1][1]:
dq.popleft()
f[r] = r * x - pre[r] + dq[0][0] * x + dq[0][1]
return f
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
N, M, P = next(data), next(data), next(data)
# 前缀距离:reach[h] = 1 号山走到 h 号山需要的时间
reach = [0] * (N + 1)
for h in range(2, N + 1):
reach[h] = reach[h - 1] + next(data)
# 每只猫只依赖 a = T - s[H]:饲养员出发不早于 a 才接得到,等待 = 出发时刻 - a
cats = [(next(data), next(data)) for _ in range(M)] # (H, T),题面顺序
limit = sorted(t - reach[h] for h, t in cats) # a_1..a_M 升序
pre = list(accumulate(limit, initial=0)) # a 的前缀和,pre[r] = sum_{i<=r} a_i
prev = [INF] * (M + 1)
prev[0] = 0 # 0 只饲养员一只猫也接不到
for _ in range(P):
cur = dp_layer(prev, limit, pre)
if cur == prev: # 已到不动点:再加饲养员结果也不会变
break
prev = cur
print(prev[M])
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:31
* update_at: 2026-10-05 12:31
*/
// 斜率优化 DP:猫压成 a = T - s[H] 后排序,P 位饲养员各接一段连续的猫,
// 转移整理成直线族(斜率 -k、截距 f[k]+S[k]),双单调用单调队列维护下凸壳。
#include <iostream>
#include <algorithm>
#include <deque>
using namespace std;
typedef long long ll;
const int MAXN = 100005; // 山和猫的数量上限
const ll INF = 4e18; // 不可达 DP 值的哨兵
ll n, m, p; // 山数、猫数、饲养员数
ll s[MAXN]; // s[h]:1 号山走到 h 号山的时间(距离前缀和)
ll a[MAXN]; // a[i]:第 i 只猫(排序后)能被接走的出发时刻下限
ll pre[MAXN]; // pre[i]:a 的前缀和
ll f[2][MAXN]; // 滚动数组:f[cur][r] = 前 r 只猫用当前位数饲养员的最小等待和
struct Cat {
ll h;
ll t;
};
Cat cats[MAXN]; // 每只猫所在山编号与玩到时刻
// 判断队尾直线 (m2,b2) 是否被新线 (m1,b1) 与队尾前一条线 (m3,b3) 夹死(生效区间为空)
// 即新线与队尾线的交点 x12 不晚于队尾线与前一条线的交点 x23,交叉相乘避免除法
bool bad_line(ll m1, ll b1, ll m2, ll b2, ll m3, ll b3) {
// x12 <= x23,即 (b2-b1)*(m2-m3) <= (b3-b2)*(m1-m2);
// b 可达 2e14、斜率差可达 1e5,乘积可超 ll,用 __int128 承接
__int128 lhs = b2 - b1;
lhs *= m2 - m3;
__int128 rhs = b3 - b2;
rhs *= m1 - m2;
return lhs <= rhs;
}
// 用上一层 DP 值 prevdp[] 推进一层:
// f[r] = min_k prevdp[k] + (r-k)*a_r - (pre[r]-pre[k])
// 把每个切点 k 看成直线 y = -k*x + (prevdp[k]+pre[k]),查询点 x = a_r
void dp_layer(ll *prevdp, ll *curdp) {
// dq 存直线 (斜率, 截距),斜率自队头向队尾递减(k 递增)
deque<pair<ll, ll> > dq;
for (ll r = 0; r <= m; r++) {
if (prevdp[r] < INF) { // 不可达切点不入壳
ll nm = -r, nb = prevdp[r] + pre[r];
// 队尾直线的生效区间被新线截空则弹出(交点比较交叉相乘,整数精确)
while (dq.size() >= 2 && bad_line(nm, nb, dq.back().first,
dq.back().second, dq[dq.size() - 2].first, dq[dq.size() - 2].second))
dq.pop_back();
dq.push_back(make_pair(nm, nb));
}
if (r == 0) {
curdp[0] = 0; // 0 只猫费用恒为 0,也是下一层 k=0 的直线来源
continue;
}
ll x = a[r]; // 查询点:段尾猫的出发时刻下限,随 r 非降
// 查询点递增,队头若已不如第二条则永远不再最优,弹出
while (dq.size() >= 2 && dq.front().first * x + dq.front().second >=
dq[1].first * x + dq[1].second)
dq.pop_front();
curdp[r] = r * x - pre[r] + dq.front().first * x + dq.front().second;
}
}
void read_input() {
cin >> n >> m >> p;
s[1] = 0;
for (ll i = 2; i <= n; i++) {
cin >> s[i];
s[i] += s[i - 1];
}
for (ll i = 1; i <= m; i++) {
cin >> cats[i].h >> cats[i].t;
}
}
void solve() {
// 每只猫压成 a = T - s[H]:饲养员不早于 a 出发才接得到,等待 = 出发时刻 - a
for (ll i = 1; i <= m; i++) {
a[i] = cats[i].t - s[cats[i].h];
}
sort(a + 1, a + m + 1);
for (ll i = 1; i <= m; i++) {
pre[i] = pre[i - 1] + a[i];
}
// 初始:0 位饲养员一只猫也接不到,只有 f[0]=0 可达
for (ll r = 1; r <= m; r++) {
f[0][r] = INF;
}
f[0][0] = 0;
ll cur = 0; // 滚动数组当前层下标
for (ll layer = 1; layer <= p; layer++) {
dp_layer(f[cur], f[cur ^ 1]);
cur ^= 1;
}
cout << f[cur][m] << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:前缀距离与前缀和
,排序 , 层 DP 每层摊还 ,共 ; ,与标准 C++ 斜率优化解法同阶。 - 空间复杂度:
(前缀距离、 、前缀和、两层 DP 数组与队列),峰值约 70MB。 - 数值:答案可达
,凸壳交点交叉相乘约 ,Python int任意精度无溢出;实测最坏数据本地约 6.3s,对比 OJ 的 1000ms 时限 Python 侧存在 TLE 风险(按任务约定允许)。
总结
- 关键观察是把每只猫压成
:位置与玩到的时刻两个字段合并成"能接它的出发时刻下限",等待时间 = 出发时刻 。 - 饲养员走完全程、无法挑猫 ⇒ 排序后每位饲养员接一段连续的猫,段费用前缀和
,问题变成至多 段的区间划分 DP。 - 转移式对切点
是一次函数,把"枚举切点"变成"直线上查询最小值";斜率 递减、查询点 非降,双单调性让下凸壳退化为双端队列,单层摊还 。 - 易错点:
与 的空段项不能丢(前者还是下一层首条直线的来源); 可相等、可为负,只要查询点非降即可,交点比较必须用整数交叉相乘。
图示解析
下图是从输入到答案的完整推导链路,每一步只做一件事:
flowchart LR A["输入 D_i, H_i, T_i"] --> B["前缀距离 s[h]"] B --> C["压参 a = T - s[H]"] C --> D["按 a 升序排序 + 前缀和 S"] D --> E["P 层分段 DP<br/>f_p[r] = min_k ..."] E --> F["提出 r 相关项<br/>切点 k 变成直线 y = -k x + b"] F --> G["单调队列维护下凸壳<br/>查询 x = a_r"] G --> H["f_P[M] 即答案"]
观察重点:排序(C→D)是"谁接谁"从集合变成连续段的分水岭;直线的斜率来自切点下标