把每个乘车请求看成区间装载,按终点升序且同终点按起点降序贪心接单,再用线段树维护路段最大占用。
OJ: luogu
题目 ID: P1607
难度:普及+/提高
标签:贪心线段树区间加区间最大值建模
日期: 2026-06-21 02:42
题意
车从 1 号站一路开到 N 号站,只走一遍,容量是 C。
有 K 个请求 (S, E, M),表示最多有 M 头牛想从 S 坐到 E。
一批牛允许只接其中一部分。
要求输出最多一共能运多少头牛。
把相邻两站之间的一段路看成一个位置,那么请求 (S, E, M) 就会同时占用区间 [S, E-1] 上的所有位置。
思路
先看一个可以直接验证想法的朴素解:
#include <bits/stdc++.h>
using namespace std;
const int MAXK = 50000 + 5;
const int MAXN = 20000 + 5;
struct Order {
int s, e, m;
};
int req_cnt, stop_cnt, cap;
Order ord[MAXK];
int used[MAXN];
long long suffix_sum[MAXK];
long long best_ans;
int calc_limit(int idx) {
int limit = ord[idx].m;
for (int i = ord[idx].s; i < ord[idx].e; i++) {
limit = min(limit, cap - used[i]);
}
return limit;
}
void add_interval(int idx, int val) {
for (int i = ord[idx].s; i < ord[idx].e; i++) {
used[i] += val;
}
}
void dfs(int idx, long long cur) {
if (idx > req_cnt) {
best_ans = max(best_ans, cur);
return;
}
// 一个简单上界:后面的所有请求都全部接上。
if (cur + suffix_sum[idx] <= best_ans) {
return;
}
int limit = calc_limit(idx);
for (int take = limit; take >= 0; take--) {
add_interval(idx, take);
dfs(idx + 1, cur + take);
add_interval(idx, -take);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 这是一个只用于小数据验证的精确暴力。
// 对每个请求枚举“到底接多少头牛”,然后搜索全局最优。
cin >> req_cnt >> stop_cnt >> cap;
for (int i = 1; i <= req_cnt; i++) {
cin >> ord[i].s >> ord[i].e >> ord[i].m;
}
suffix_sum[req_cnt + 1] = 0;
for (int i = req_cnt; i >= 1; i--) {
suffix_sum[i] = suffix_sum[i + 1] + ord[i].m;
}
dfs(1, 0);
cout << best_ans << '\n';
return 0;
}brute.cpp 直接枚举每个请求到底接多少头牛,并维护每一段路的占用情况。
它是精确解,但只能用于小数据对拍,因为状态数会随着请求数快速爆炸。
这题真正的关键在于贪心顺序:
- 终点
E小的先处理 - 如果终点相同,起点
S大的先处理
同终点的第二条很重要,这张表展示了一个反例:
| 请求 | 区间 | 数量 |
|---|---|---|
| A | [1, 3) |
1 |
| B | [2, 3) |
1 |
| C | [1, 2) |
1 |
车容量设为 1。
如果先处理 A,它会同时占住前后两段路,后面的 B 和 C 最多只能再选一个,总答案只有 1。
但如果先处理更短的 B,再处理 C,总答案能达到 2。
所以同终点时,必须让起点更大的请求先处理。
顺序确定后,处理当前请求时就应该尽量多接。 因为每头牛贡献都一样,而当前请求在贪心顺序里优先级最高,没必要故意留容量给更晚结束或更长的请求。
设 used[i] 表示第 i 段路当前已经坐了多少头牛。
对于请求 (S, E, M):
- 它经过的区间是
[S, E-1] - 这段区间里最满的一段若为
mx - 那么这次最多还能接
min(M, C - mx)头牛
于是问题变成了标准的两件事:
- 区间最大值查询
- 区间整体加法修改
用懒标记线段树维护即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXK = 50000 + 5;
const int MAXN = 20000 + 5;
struct Order {
int s, e, m;
};
int req_cnt, stop_cnt, cap;
Order ord[MAXK];
int seg_max[MAXN << 2];
int lazy_add[MAXN << 2];
bool cmp_order(const Order &a, const Order &b) {
if (a.e != b.e) {
return a.e < b.e;
}
// 同终点时,起点更大的请求更短,应该优先处理。
return a.s > b.s;
}
void push_up(int u) {
seg_max[u] = max(seg_max[u << 1], seg_max[u << 1 | 1]);
}
void apply_add(int u, int val) {
seg_max[u] += val;
lazy_add[u] += val;
}
void push_down(int u) {
if (lazy_add[u] == 0) {
return;
}
apply_add(u << 1, lazy_add[u]);
apply_add(u << 1 | 1, lazy_add[u]);
lazy_add[u] = 0;
}
void range_add(int u, int l, int r, int ql, int qr, int val) {
if (ql <= l && r <= qr) {
apply_add(u, val);
return;
}
push_down(u);
int mid = (l + r) >> 1;
if (ql <= mid) {
range_add(u << 1, l, mid, ql, qr, val);
}
if (qr > mid) {
range_add(u << 1 | 1, mid + 1, r, ql, qr, val);
}
push_up(u);
}
int query_max(int u, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) {
return seg_max[u];
}
push_down(u);
int mid = (l + r) >> 1;
int ans = 0;
if (ql <= mid) {
ans = max(ans, query_max(u << 1, l, mid, ql, qr));
}
if (qr > mid) {
ans = max(ans, query_max(u << 1 | 1, mid + 1, r, ql, qr));
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> req_cnt >> stop_cnt >> cap;
for (int i = 1; i <= req_cnt; i++) {
cin >> ord[i].s >> ord[i].e >> ord[i].m;
}
sort(ord + 1, ord + req_cnt + 1, cmp_order);
long long ans = 0;
for (int i = 1; i <= req_cnt; i++) {
int l = ord[i].s;
int r = ord[i].e - 1;
// 每个请求会同时占用 [s, e-1] 上的所有路段。
// 还能再塞多少头牛,只取决于这段区间里最满的那一段。
int cur_max = query_max(1, 1, stop_cnt - 1, l, r);
int can_take = min(ord[i].m, cap - cur_max);
if (can_take <= 0) {
continue;
}
ans += can_take;
range_add(1, 1, stop_cnt - 1, l, r, can_take);
}
cout << ans << '\n';
return 0;
}复杂度
排序复杂度是
之后每个请求需要一次区间最大值查询和最多一次区间加法修改,所以总时间复杂度是
线段树空间复杂度是
总结
这题最核心的转换是:
- 把“从站点
S坐到E”变成“占用路段区间[S, E-1]”
然后抓住两个关键点:
- 按终点升序贪心
- 同终点按起点降序
最后再用线段树把“区间最满路段”和“整段加人”维护起来,整题就完成了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
