[CSP-S 2024] 超速检测

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

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

OJ: luogu

题目 ID: P11232

难度:普及+/提高

标签:贪心二分区间覆盖

日期: 2026-06-22 18:29

题意

有一条长度为 L 的道路,限速为 V。第 i 辆车从位置 d_i 驶入,初速度为 v_i,加速度为 a_i。道路上有 m 个测速仪,位置为递增的 p_j

如果一辆车经过某个开启的测速仪时,瞬时速度严格超过 V,它就会被判定超速。

需要输出:

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

思路

先看一个直接暴力:对每辆车枚举所有测速仪,判断是否超速;然后枚举保留哪些测速仪,找覆盖所有超速车的最小保留集合。

cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 20;

int T;
int n, m;
long long L, V;
long long d[MAXN], v[MAXN], a[MAXN], p[MAXN];
vector<int> cover[MAXN];

bool is_speeding(long long id, long long pos) {
    long long value = v[id] * v[id] + 2LL * a[id] * (pos - d[id]);
    return value > 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;
                for (int k = 0; k < (int)cover[i].size(); 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) 枚举测速仪子集。

我们先处理第一件事:一辆车会在哪些测速仪处超速。

根据题目给出的公式,车辆在位置 x 的速度平方为:

text
v_i^2 + 2a_i(x - d_i)

判断超速只需要比较:

text
v_i^2 + 2a_i(x - d_i) > V^2

不用开方,也不用浮点数。

因为这个式子关于 x 是单调的:

  • a_i = 0:速度不变;
  • a_i > 0:速度随位置变大而变大,超速测速仪是一个后缀;
  • a_i < 0:速度随位置变大而变小,超速测速仪是一个前缀。

所以每辆会被检测到的车,都可以转成测速仪下标上的一个连续区间 [l,r]

求区间时,先用 lower_bound 找到第一台位置不小于 d_i 的测速仪。然后分三类:

  • 匀速车:若 v_i > V,区间是 [start,m]
  • 加速车:二分第一个超速测速仪,区间是 [first,m]
  • 减速车:若起点处已经不超速,则无区间;否则二分最后一个超速测速仪,区间是 [start,last]

第一问就是非空区间的数量。

第二问变成:给定若干闭区间,选择尽量少的测速仪下标,使每个区间至少包含一个被选下标。

这是标准区间贪心:把区间按右端点从小到大排序。扫描时,如果当前已经选择的最后一个点不在当前区间内,就选择当前区间的右端点。这样既覆盖当前区间,又尽量靠右,最有机会覆盖后面的区间。

设最少需要保留 required 个测速仪,那么最多能关闭:

text
m - required

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;

int T;
int n, m;
long long L, V;
long long d[MAXN], v[MAXN], a[MAXN];
long long p[MAXN];
vector<pair<int, int> > intervals;

bool cmp_interval(const pair<int, int> &x, const pair<int, int> &y) {
    if (x.second != y.second) {
        return x.second < y.second;
    }
    return x.first < y.first;
}

bool is_speeding(long long d, long long v, long long a, long long pos) {
    long long value = v * v + 2LL * a * (pos - d);
    return value > V * V;
}

void add_interval(long long d, long long v, long long a) {
    int start = lower_bound(p + 1, p + m + 1, d) - p;
    if (start > m) {
        return;
    }

    if (a == 0) {
        if (v > V) {
            intervals.push_back(make_pair(start, m));
        }
        return;
    }

    if (a > 0) {
        int left = start, right = m;
        int first = m + 1;

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

        if (first <= m) {
            intervals.push_back(make_pair(first, m));
        }
        return;
    }

    if (v <= V || !is_speeding(d, v, a, p[start])) {
        return;
    }

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

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

    intervals.push_back(make_pair(start, last));
}

int min_required_sensors() {
    // 区间按右端点排序,贪心选择当前区间的右端点。
    sort(intervals.begin(), intervals.end(), cmp_interval);

    int selected = 0;
    int last_pos = 0;
    for (int i = 0; i < (int)intervals.size(); i++) {
        if (last_pos < intervals[i].first) {
            selected++;
            last_pos = intervals[i].second;
        }
    }

    return selected;
}

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

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

        intervals.clear();

        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 speeding_cars = (int)intervals.size();
        int required = min_required_sensors();

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

    return 0;
}

复杂度

每辆车用二分求区间,复杂度为 O(logm)O(log m)。所有区间排序复杂度为 O(nlogn)O(n log n)

总时间复杂度为 O(nlogm+nlogn)O(n log m + n log n),空间复杂度为 O(n+m)O(n + m)

总结

这题的第一步是把物理公式离散化到测速仪位置上:用速度平方和 V^2 比较,避免浮点误差。

第二步是把每辆被检测到的车压成一个区间。只要得到区间,关闭测速仪的问题就变成“最少点覆盖所有区间”,按右端点贪心即可。