把总旅行时间改写为按下车点加权的到达时刻和,贪心把加速器投给单位收益最大的边,再用饱和事件把逐个加速压成 O(n²) 批量执行。
OJ: roj
题目 ID: 3593
难度:提高
标签:贪心模拟python
创建: 2026-10-02 09:34
更新: 2026-10-10 11:30
形式化题目
样例(
| 时刻 | 事件 | 说明 |
|---|---|---|
| 0 | 公交到达景点 1 | 站 1 有乘客 1 分钟才到,等到 1 分发车 |
| 2 | 到达景点 2 | 发车时刻 |
| 5 | 从景点 2 发车 | 站 2 有乘客 5 分钟才到, |
| 7 | 到达景点 3 | 对 |
于是三位乘客的旅行时间为
正解
思路
先把目标从"乘客"换成"站点"。 乘客的
朴素做法与瓶颈。 最容易想到的贪心是:重复
关键观察一:提前量沿链条传播,被"等待余量"截断。 记
flowchart TD
A["第 i 段 D_i 减 1"] --> B["站 i+1 到达时刻提前 1"]
B --> C{"站 j 还有等待余量吗<br/>ahead[j] < cap[j]"}
C -- 是:发车也提前 1 --> D["提前量继续传给站 j+1"]
D --> C
C -- 否:饱和站,等待吃掉提前量 --> E["链条截断<br/>只保留站 j 自身的收益"]
关键观察二:单位收益只由饱和分布决定,
其中
关键观察三:批量加速,轮数只与饱和事件有关。 一次给选中的边注入
- 某站首次饱和——全体站最多饱和
次; - 或该段耗尽(
)——最多 次; - 或加速器用完。
每轮必发生其中一种,故总轮数
样例上的执行过程(
| 轮次 | 收益链条 | 边 1 收益 | 边 2 收益 | 决策 |
|---|---|---|---|---|
| 1 | 边 1 止于饱和站 2;边 2 链到终点站 3 | 选边 2,批量注入 |
||
| 2 | 加速器已用完 | — | — | 停止,输出 |
站 2 一开始就是饱和站(
正确性上,目标对每段注入是凹的:链条上的
代码
/**
* 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
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INFLL = (1LL<<60);
int n, tourists, k; vector<ll> d, waitv, alightv, capv, prefixv, aheadv, usedv;
vector<int> next_saturated(){ vector<int> sat(n+2,n+1); for(int j=n;j>=2;j--) sat[j]=(aheadv[j]>=capv[j]?j:sat[j+1]); return sat; }
int pick_edge(vector<int> &sat){ int best=0; ll gain=0; for(int i=1;i<(int)d.size();i++){ if(usedv[i]>=d[i]) continue; int stop=sat[i+1]; if(stop>n) stop=n; ll gi=prefixv[stop]-prefixv[i]; if(gi>gain){best=i; gain=gi;} } return best; }
ll batch_limit(int start, ll budget){ ll gate=INFLL,event=INFLL; for(int j=start+1;j<(int)aheadv.size();j++){ if(aheadv[j]>=capv[j]) break; ll room=capv[j]-aheadv[j]; if(room<=gate) event=min(event, room); gate=min(gate, room); } return min(budget,event); }
int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); if(!(cin>>n>>tourists>>k)) return 0; d.assign(n,0); for(int i=1;i<n;i++) cin>>d[i]; waitv.assign(n+1,0); alightv.assign(n+1,0); ll total_t=0; for(int i=0;i<tourists;i++){ll t; int a,b; cin>>t>>a>>b; waitv[a]=max(waitv[a],t); alightv[b]++; total_t += t;} vector<ll> arrive(n+1); for(int i=1;i<n;i++) arrive[i+1]=max(arrive[i],waitv[i])+d[i]; capv.assign(n+1,0); for(int i=2;i<=n;i++) capv[i]=max(arrive[i]-waitv[i],0LL); prefixv.assign(n+1,0); for(int j=2;j<=n;j++) prefixv[j]=prefixv[j-1]+alightv[j]; aheadv.assign(n+1,0); usedv.assign(n+1,0); int remain=k; while(remain>0){ vector<int> sat=next_saturated(); int edge=pick_edge(sat); if(edge==0) break; ll budget=min((ll)remain, d[edge]-usedv[edge]); ll step=batch_limit(edge,budget); usedv[edge]+=step; remain-=step; ll delta=step; for(int j=edge+1;j<=n;j++){ ll room=capv[j]-aheadv[j]; aheadv[j]+=delta; delta=min(delta, room); if(delta<=0) break; } }
ll base=0,saved=0; for(int j=2;j<=n;j++){ base += arrive[j]*alightv[j]; saved += aheadv[j]*alightv[j]; } cout << base-total_t-saved << '\n'; 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-02 09:34
# update_at: 2026-10-02 09:34
import sys
INF = 10**18 # "不会发生"的事件时刻:链条上没有任何饱和站点
def next_saturated(ahead: list[int], cap: list[int]) -> list[int]:
"""sat[j] = ≥j 的最近饱和景点编号,n+1 表示 j 往后一路畅通。"""
n = len(ahead) - 1
sat = [n + 1] * (n + 2)
for j in range(n, 1, -1):
sat[j] = j if ahead[j] >= cap[j] else sat[j + 1]
return sat
def pick_edge(sat: list[int], prefix: list[int], d: list[int], used: list[int]) -> int:
"""在还有加速名额的边里选单位收益最大的一条:收益 = 链条下游的下车人数;无正收益返回 0。"""
best, gain = 0, 0
for i in range(1, len(d)):
if used[i] >= d[i]:
continue # 这段已减到 0,不能再加速
stop = sat[i + 1] # 收益链条的终点:最近的饱和景点
if stop >= len(prefix): # 没有饱和站点,链到终点站
stop = len(prefix) - 1
gain_i = prefix[stop] - prefix[i] # 环节 [i+1, stop] 的下车人数
if gain_i > gain:
best, gain = i, gain_i
return best
def batch_limit(ahead: list[int], cap: list[int], start: int, budget: int) -> int:
"""start 段一次能批量加多少个加速器:到"第一次饱和截断收益"为止,或用完 budget。"""
gate = INF # 链条上游各站还能放行的提前量下限
event = INF # 第一次让某站恰好饱和所需的提前量
for j in range(start + 1, len(ahead)):
if ahead[j] >= cap[j]:
break # 已饱和:本站之后链条已断,不再有新事件
room = cap[j] - ahead[j] # 本站还能吃下的提前量
if room <= gate:
event = min(event, room) # 加满 room 本站就饱和
gate = min(gate, room)
return min(budget, event)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, tourists, k = next(data), next(data), next(data)
d = [0] + [next(data) for _ in range(n - 1)] # d[i]:景点 i → i+1 的原始车程
wait = [0] * (n + 1) # wait[i]:在景点 i 上车的最晚到达时刻(车要等齐)
alight = [0] * (n + 1) # alight[j]:在景点 j 下车的乘客数
total_t = 0
for _ in range(tourists):
t, a, b = next(data), next(data), next(data)
wait[a] = max(wait[a], t)
alight[b] += 1
total_t += t
# 不加速时的到达时刻:到站后等到最晚乘客才发车
arrive0 = [0] * (n + 1)
for i in range(1, n):
arrive0[i + 1] = max(arrive0[i], wait[i]) + d[i]
# cap[i] = 等待余量:到达时刻提前量不超过它时发车跟着提前,超过的部分被等待吃掉
cap = [0] * (n + 1)
for i in range(2, n + 1):
cap[i] = max(arrive0[i] - wait[i], 0)
prefix = [0] * (n + 1) # prefix[x] = Σ_{j≤x} alight[j]
for j in range(2, n + 1):
prefix[j] = prefix[j - 1] + alight[j]
ahead = [0] * (n + 1) # ahead[i]:第 i 段已累计提前的分钟数
used = [0] * (n + 1) # used[i]:第 i 段已用掉的加速器数
remain = k
while remain > 0:
edge = pick_edge(next_saturated(ahead, cap), prefix, d, used)
if edge == 0:
break # 没有正收益的边,再加速也无用
budget = min(remain, d[edge] - used[edge])
step = batch_limit(ahead, cap, edge, budget)
used[edge] += step
remain -= step
# 提前量沿链条向下游传播:每过一站最多被该站的等待余量吃掉
delta = step
for j in range(edge + 1, n + 1):
room = cap[j] - ahead[j]
ahead[j] += delta
delta = min(delta, room)
if delta <= 0:
break
base = sum(arrive0[j] * alight[j] for j in range(2, n + 1)) - total_t
saved = sum(ahead[j] * alight[j] for j in range(2, n + 1))
print(base - saved)
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
。预处理 ;主循环每轮 ,每轮至少触发一次"新饱和 / 边耗尽",总轮数 。相比朴素的 ,消掉了 的因子。 - 空间复杂度:
,约十个长度为 的数组, 时仅几十 KB。 - 实测:20 组真实数据全部一致,单点 0.016–0.043 s、峰值内存
17 MB,相对 1000 ms / 32 MB 的限制十分宽裕。
总结
这道题的三层转化是关键:总旅行时间
实现时注意三点:截断要用注入前的余量 cap[j] - ahead[j] 计算;饱和站自身的到达时刻仍算收益,链条"止于且包含"第一个饱和站;加速器耗尽或所有边收益为 0 时立即退出,避免无意义的空转。实测 20 组真实数据全部一致,样例输出