[USACO09FEB] Fair Shuttle G

GitHub跳转原题关系图返回列表

把每个乘车请求看成区间装载,按终点升序且同终点按起点降序贪心接单,再用线段树维护路段最大占用。

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] 上的所有位置。

思路

先看一个可以直接验证想法的朴素解:

cpp
#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,它会同时占住前后两段路,后面的 BC 最多只能再选一个,总答案只有 1。 但如果先处理更短的 B,再处理 C,总答案能达到 2。 所以同终点时,必须让起点更大的请求先处理。

顺序确定后,处理当前请求时就应该尽量多接。 因为每头牛贡献都一样,而当前请求在贪心顺序里优先级最高,没必要故意留容量给更晚结束或更长的请求。

used[i] 表示第 i 段路当前已经坐了多少头牛。 对于请求 (S, E, M)

  • 它经过的区间是 [S, E-1]
  • 这段区间里最满的一段若为 mx
  • 那么这次最多还能接 min(M, C - mx) 头牛

于是问题变成了标准的两件事:

  • 区间最大值查询
  • 区间整体加法修改

用懒标记线段树维护即可。

代码

cpp
#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;
}

复杂度

排序复杂度是 O(KlogK)O(K log K)

之后每个请求需要一次区间最大值查询和最多一次区间加法修改,所以总时间复杂度是 O(KlogN)O(K log N)

线段树空间复杂度是 O(N)O(N)

总结

这题最核心的转换是:

  • 把“从站点 S 坐到 E”变成“占用路段区间 [S, E-1]

然后抓住两个关键点:

  • 按终点升序贪心
  • 同终点按起点降序

最后再用线段树把“区间最满路段”和“整段加人”维护起来,整题就完成了。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析