[CSP-S 2024] 超速检测

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

用速度平方把每辆车能被测出超速的测速仪转成区间,再按右端点贪心求最少保留测速仪。

OJ: luogu

题目 ID: P11232

难度:提高+/省选-

标签:贪心二分区间覆盖

创建: 2026-06-22 18:29

更新: 2026-10-07 12:15

形式化题目

一条长度为 LL 的直路上有 nn 辆车。第 ii 辆车从位置 did_i 以初速度 vi>0v_i>0、加速度 aia_i(可正可负可零)向北做匀加速运动,到达 LL 或速度降为 00 时驶离。

路上有 mm 台测速仪,位置递增:0⩽p1<⋯<pm⩽L0\leqslant p_1<\cdots<p_m\leqslant L。车经过某台开启的测速仪时,瞬时速度严格超过限速 VV 就被判定超速。

求两个数:

  1. 全部测速仪都开启时,有多少辆车会被判定超速;
  2. 在不漏掉这些超速车的前提下,最多能关闭多少台测速仪。

暴力解法

思路

先对每辆车枚举所有测速仪,记录它会在哪些测速仪处超速;然后枚举保留哪些测速仪,检查是否每辆超速车都至少被测到一次,取保留台数最少的方案。

代码

cpp
/**
 * 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-01 22:42
 * update_at: 2026-10-01 22:42
 */
// brute.cpp:小数据暴力解,逐车逐测速仪判断超速,再枚举测速仪子集求最少保留台数。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 20;   // 暴力只服务小数据,m 也不能大(要枚举 2^m 个子集)

int T;
int n, m;
ll L, V;                      // L 道路长度,V 限速
ll d[MAXN], v[MAXN], a[MAXN]; // 每辆车的驶入位置、初速度、加速度
ll p[MAXN];                   // 测速仪位置
vector<int> cover[MAXN];      // cover[i]:能测出第 i 辆车超速的测速仪下标

// 判断第 id 辆车在位置 pos 处是否超速,比较速度平方即可。
bool is_speeding(ll id, ll pos) {
    ll speed_square = v[id] * v[id] + 2 * a[id] * (pos - d[id]);
    return speed_square > V * V;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> T;
    while (T--) {
        cin >> n >> m >> L >> V;

        for (int i = 1; i <= n; i++) {
            cin >> d[i] >> v[i] >> a[i];
            cover[i].clear();
        }
        for (int j = 1; j <= m; j++) {
            cin >> p[j];
        }

        // 先逐车找出所有能测出它超速的测速仪。
        int speeding_cars = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (p[j] < d[i]) {
                    continue;   // 车还没驶入道路
                }
                if (is_speeding(i, p[j])) {
                    cover[i].push_back(j);
                }
            }
            if (!cover[i].empty()) {
                speeding_cars++;
            }
        }

        // 枚举保留哪些测速仪,找出能覆盖全部超速车的最少台数。
        int best_keep = m;
        int total = 1 << m;

        for (int mask = 0; mask < total; mask++) {
            bool ok = true;

            for (int i = 1; i <= n && ok; i++) {
                if (cover[i].empty()) {
                    continue;
                }

                bool caught = false;
                int cnt = cover[i].size();
                for (int k = 0; k < cnt; k++) {
                    int sensor = cover[i][k] - 1;
                    if (mask & (1 << sensor)) {
                        caught = true;
                        break;
                    }
                }
                if (!caught) {
                    ok = false;
                }
            }

            if (ok) {
                int keep = 0;
                for (int j = 0; j < m; j++) {
                    if (mask & (1 << j)) {
                        keep++;
                    }
                }
                best_keep = min(best_keep, keep);
            }
        }

        cout << speeding_cars << ' ' << m - best_keep << '\n';
    }

    return 0;
}

复杂度

判断部分 O(nm)O(nm);枚举保留子集是 O(2m)O(2^m),每个子集再检查 O(nm)O(nm)。总时间复杂度 O(2m⋅nm)O(2^m\cdot nm),空间 O(nm)O(nm)。

瓶颈

mm 可以到 10510^5,2m2^m 完全不可行;nmnm 也只能撑住 n,m⩽103n,m\leqslant 10^3 的档位。真正要解决的是两件事:不要逐个测速仪判断,也不要枚举子集。

正解

思路

用速度平方比较,避开浮点

根据题目给的公式,车在位置 xx 的速度平方是

vi2+2ai(x−di). v_i^2+2a_i(x-d_i).

超速等价于瞬时速度严格大于 VV。两边平方(都非负)得

vi2+2ai(x−di)>V2. v_i^2+2a_i(x-d_i)>V^2.

全程只用整数比较,既不用开方也不用浮点数,没有精度问题。

即使 ai<0a_i<0 时车会在某处停下,这个式子也自动处理:停车之后速度平方是负数,不可能大于 V2V^2。

每辆车的超速测速仪是一段连续区间

式子关于 xx 是一次函数,所以随 xx 单调:

加速度 速度变化 超速测速仪的形态
ai=0a_i=0 不变 要么全程超速,要么全程不超速
ai>0a_i>0 越走越快 一个后缀
ai<0a_i<0 越走越慢 一个前缀

再注意到车只在 x⩾dix\geqslant d_i 的路段上,所以超速测速仪的下标是 [1,m][1,m] 中的一段连续区间。

用 lower_bound 找到第一台不早于 did_i 的测速仪 startstart,然后:

  • 匀速:vi>Vv_i>V 则区间是 [start,m][start,m],否则没有区间;
  • 加速:二分第一个超速的测速仪 firstfirst,区间是 [first,m][first,m];
  • 减速:若 startstart 处已不超速则没有区间,否则二分最后一个超速的 lastlast,区间是 [start,last][start,last]。

有区间的车就是会被判定超速的车,所以第一问就是非空区间的个数。

第二问:最少点覆盖区间

要关掉尽量多的测速仪,等于保留尽量少的测速仪,使得每个区间至少含一个保留点。这是经典的最少点覆盖区间问题。

贪心策略:把区间按右端点从小到大看,如果当前区间还没被覆盖,就选它的右端点。选右端点最靠右,最有机会把后面的区间一起盖住,可以证明这样是最优的。

右端点天然在 [1, m],不必排序

通常做法要对区间排序。但这里区间的右端点就是测速仪下标,本来就在 [1,m][1,m],所以用“按右端点分桶”代替排序:

text
max_left[r] = 右端点为 r 的区间里最大的左端点

然后 r=1,2,…,mr=1,2,\ldots,m 从小到大扫一遍:只要 max_left[r]max\_left[r] 大于已选的最右位置,就选 rr。同一个右端点上只要有一个区间没被覆盖,选 rr 就能把它们全部盖住,所以只留最大的左端点就够,区间表本身都不用落地保存。

这样贪心部分是 O(n+m)O(n+m),比排序的 O(nlog⁡n)O(n\log n) 更省。

样例推演

样例 11:n=5,m=5,L=15,V=3n=5,m=5,L=15,V=3,测速仪在 2,5,8,9,152,5,8,9,15。每辆车的超速区间:

车 di,vi,aid_i,v_i,a_i 超速测速仪下标 说明
1 0,3,00,3,0 无 匀速 33,不超速
2 12,4,012,4,0 [5,5][5,5] 匀速 4>34>3,只有位置 1515 的测速仪
3 1,1,41,1,4 [2,5][2,5] 加速;位置 22 处速度恰为 33 不算超速,55 之后全部超速
4 5,5,−25,5,-2 [2,3][2,3] 减速;位置 99 处速度恰为 33,只在 5,85,8 两处超速
5 6,4,−46,4,-4 无 在位置 6786\tfrac78 前就降到 33,这段没有测速仪

第一问答案是 33(车 2、3、4)。

第二问先按右端点分桶:max_left[3]=2max\_left[3]=2(来自车 4 的 [2,3][2,3]),max_left[5]=5max\_left[5]=5(车 2 的 [5,5][5,5] 与车 3 的 [2,5][2,5] 取大)。然后从左到右扫:

text
r = 3: 2 > 0   -> 选 3
r = 5: 5 > 3   -> 选 5

最少保留 22 台(例如位置 88 与 1515 两台,位置 55 与 1515 也行),最多关闭 5−2=35-2=3 台,与样例一致。

代码

cpp
/**
 * 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-01 22:48
 * update_at: 2026-10-01 22:48
 */
// main.cpp:把每辆车能被测出超速的测速仪压成区间,再按右端点贪心求最少保留测速仪。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 100005;

int T;
int n, m;
ll L, V;                      // L 道路长度,V 限速
ll d[MAXN], v[MAXN], a[MAXN]; // 每辆车的驶入位置、初速度、加速度
ll p[MAXN];                   // 测速仪位置,保证严格递增

int speeding_cars;            // 超速车数量,也就是产生了区间的车辆数
int max_left[MAXN];           // max_left[r]:右端点为 r 的区间里最大的左端点

// 判断从 start_pos 以 start_speed 驶入、加速度为 acc 的车在位置 pos 处是否超速。
// 直接比较速度平方 v^2 + 2*acc*(pos - start_pos) 与 V^2,避免开方和浮点误差。
bool is_speeding(ll start_pos, ll start_speed, ll acc, ll pos) {
    ll speed_square = start_speed * start_speed + 2 * acc * (pos - start_pos);
    return speed_square > V * V;
}

// 记录一个测速仪下标区间 [left, right]。
// 后面的贪心只关心“同一个右端点上最大的左端点”,所以只保留这个值,不落地存整张区间表。
void record_interval(int left, int right) {
    if (max_left[right] < left) {
        max_left[right] = left;
    }
    speeding_cars++;
}

// 求出这辆车会在哪些测速仪处超速,把对应的连续下标区间记下来。
void add_interval(ll start_pos, ll start_speed, ll acc) {
    // 测速仪位置递增,先找第一台不早于驶入位置的测速仪。
    // 测速仪都在道路内(p[j] <= L),所以不用再按 L 截断。
    int start = lower_bound(p + 1, p + m + 1, start_pos) - p;
    if (start > m) {
        return;
    }

    if (acc == 0) {
        // 匀速车:速度一直不变,要么全程超速,要么全程不超速。
        if (start_speed > V) {
            record_interval(start, m);
        }
        return;
    }

    if (acc > 0) {
        // 加速车:速度随位置单调变大,超速测速仪构成一个后缀,二分第一个超速的。
        int left = start, right = m;
        int first = m + 1;

        while (left <= right) {
            int mid = (left + right) / 2;
            if (is_speeding(start_pos, start_speed, acc, p[mid])) {
                first = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        if (first <= m) {
            record_interval(first, m);
        }
        return;
    }

    // 减速车:速度随位置单调变小,超速测速仪构成一个前缀。
    // 先确认起点处就已超速,再二分最后一个超速的测速仪。
    if (start_speed <= V || !is_speeding(start_pos, start_speed, acc, p[start])) {
        return;
    }

    int left = start, right = m;
    int last = start;

    while (left <= right) {
        int mid = (left + right) / 2;
        if (is_speeding(start_pos, start_speed, acc, p[mid])) {
            last = mid;
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    record_interval(start, last);
}

// 最少保留多少台测速仪,才能让每辆超速车至少被测到一次。
// 标准区间贪心是按右端点排序后选右端点;这里右端点本身就是 [1, m] 的测速仪下标,
// 按右端点分桶后直接从小到大扫,省掉排序。
int min_required_sensors() {
    int selected = 0;
    int last_pos = 0;

    for (int r = 1; r <= m; r++) {
        // 右端点为 r 的区间里只要有一个还没被覆盖,就必须选 r。
        // 选 r 最靠右,最有机会把后面的区间一起盖住。
        if (max_left[r] > last_pos) {
            selected++;
            last_pos = r;
        }
    }

    return selected;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> T;
    while (T--) {
        cin >> n >> m >> L >> V;

        speeding_cars = 0;
        for (int i = 1; i <= m; i++) {
            max_left[i] = 0;
        }

        for (int i = 1; i <= n; i++) {
            cin >> d[i] >> v[i] >> a[i];
        }
        for (int i = 1; i <= m; i++) {
            cin >> p[i];
        }

        for (int i = 1; i <= n; i++) {
            add_interval(d[i], v[i], a[i]);
        }

        int required = min_required_sensors();

        cout << speeding_cars << ' ' << m - required << '\n';
    }

    return 0;
}

复杂度

  • 时间复杂度:O(nlog⁡m+n+m)O(n\log m+n+m)。每辆车二分求区间 O(log⁡m)O(\log m),贪心线性扫测速仪。
  • 空间复杂度:O(n+m)O(n+m)。

总结

这题分两步压缩:

  1. 用 vi2+2ai(x−di)>V2v_i^2+2a_i(x-d_i)>V^2 把物理过程变成整数比较,并利用一次函数的单调性把每辆车压成一段测速仪下标区间;
  2. 把“最多关多少台”变成“最少点覆盖区间”,按右端点贪心。

实现上还有一个小技巧:区间右端点本来就在 [1,m][1,m],所以按右端点分桶就能代替排序,而且只用记每个右端点上最大的左端点,区间表不必落地保存。

本文的 main.cpp 已用枚举子集的 brute.cpp 对拍 600600 组随机数据(含更宽的加速度与限速取值),题面样例通过。