问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

CSP-S 2024 超速检测题解:二分查找与贪心算法的完美结合

创作时间:
作者:
@小白创作中心

CSP-S 2024 超速检测题解:二分查找与贪心算法的完美结合

引用
CSDN
1.
https://blog.csdn.net/weixin_43863957/article/details/143429893

本文详细解析了CSP-S 2024竞赛中的一道算法题"超速检测",通过二分查找和贪心算法的结合,展示了如何高效解决复杂问题。文章适合有一定算法基础的读者学习参考。

题目概述

难度:普及+/提高
考点:二分、贪心

题意
主干道长度为L,有n辆车,看做左端点为0,第i辆车距离左端点处d_i处驶入,初速度为v_{0_i},加速度为a_i,当瞬间速度为0/到达右端点L时驶离主干道。

m个测试仪,第j个测试仪位于右端点P_j处,每个测试仪可以启动/关闭。某辆车如果在启动的测速仪上瞬间速度超过了限速V,则说明超速了,注意起点和终点都有可能会有测速仪,(有可能驶入主干道就超速了)。

题目所求
第一问:如果全部测速仪启动,n辆车中会有多少辆车超速了。
第二问:如果想关闭一部分测试仪,且不漏掉记录超速的车辆情况下,最多可以关闭多少个测速仪。

贴心的准备了加速度相关的公式(虽然只有一个有用);

解题思路

Subtask 1、2:暴力枚举

对于n, m ≤ 20的情况,可以暴力枚举每个测速仪的开/关状态,共2^m种选择。再用m × n来检查答案,但2^{20} \times n \times m \le 4e8。尝试将n × m优化到O(n)。

分类讨论

其中p_j表示车i在主干道上「首个」测速仪的位置,p_m表示主干道最后一个测速仪的位置。

  • 匀速:最大速度在[p_j, p_m]一样,从头到尾速度不变,所以判断区间内任意一个测速仪都可以。
  • 加速:最大速度在测速仪p_m位置,由于是匀加速运动,则整段路中最后一个测速仪时速度值最大,如果没有超速,那么主干道上都没超速。
  • 减速:最大速度在测速仪p_j位置,由于是匀减速运动,则整段路中首个遇到的测速仪速度值最大。

通过二分来加速查找p_j「首个」测速仪的位置。

int j = std::lower_bound( p.begin(), p.end(), d[i] ) - p.begin();

时间复杂度优化到O(nlogm) * 2^m,20pts到手。

Subtask 3、4:匀速运动

对于a_i = 0(匀速运动)的情况,直接判断p_j ∼ p_m其中一个测速仪即可,不妨选择p_m,如果p_m ≥ d_i && v_i > V。说明车辆i在整段路都超速了,答案就是m - j + 1测速仪。

正解(100pts)

对于上一个问题进行深入分析:什么样的测速仪能检测车i超速。

不妨设f_i:表示车i进入主干道「首个」测速仪的下标。

  • 匀速,[f_i, m],所有碰到的测速仪都可以检测到。
  • 加速,[f_i, m]的后缀,如上图,匀加速运动,相当于一整段主干道速度都在提升,有可能是加速一段之后「瞬时速度」大于V碰到首个摄像头P_{k1},由于P_{k1} ≥ f_i 故这一段肯定属于[f_i, m]的「后缀」,实际可以表示为[p_{k1}, m]。
  • 减速,[f_i, m]的前缀,如上图,与加速相反,有可能一开始已经在超速,经过一段时间的减速后「瞬时速度」小于V碰到首个摄像头P_{k2},由于P_{k2} ≤ m故这一段肯定属于[f_i, m]的「前缀」,实际可以表示为[f_i, p_{k2}]。

可以发现符合要求的是连续的一段区间[f_i, m],(符合要求):拍摄到超速测速仪,也好求(二分)求「前缀 / 后缀」。

二分的细节

check:第i车走j测速仪有无超速

可以发现题目中给了几个公式,但实际上有用的只有一个:瞬时速度:\sqrt{ v_0^2 + 2 \times a \times s }

二分就是

check:每辆车i在(初始速度为V_0,距离起点为d_i,加速度为a_i)的瞬时速度:\sqrt{ V_0^2 + 2 \times a \times( p_j-d_i ) } > V。

根号会出现误差,最直接的办法就是想办法出掉精度丢失上带来的误差,直接两边平方,V_0^2 + 2 \times a \times (p_j-d_i) > V^2。

还要计算一下答案是否会超出

int,10^6 + 2*10^9 \le 2147483647(int)。

(如上图所示,答案就是一段一段连续的区间,代表某辆车被检测到超速的测速仪区间)

问1:区间的个数(超速车的数量)。

问2:保留若干个点,便得每个区间至少有一个点。

第二个问题就很眼熟是吧(入门贪心:区间选点问题?),我们可以用(贪心)→(区间的右端点排序),如下图。

作为OI教练一般不刻意压行,应该代码分块展示,会让读者/学生思路更加清晰。

参考代码

#include <bits/stdc++.h>
#define ll long long
const int N = 1e5 + 10;
int n, m, L, V, d[N], v[N], a[N];
std::vector<int> p; // 寄存测速仪
struct Seg
{
    int l, r;                    // 记录车 i 会被测速仪,检测到超速的区间 [l,r]
    bool operator<(const Seg &a) // 贪心 ::: 右端点排序
    {
        return r < a.r;
    }
};
std::vector<Seg> seg;
// 计算瞬间速度
ll speed(ll v, ll a, ll s)
{
    return v * v + 2 * a * s; // 题目已经给出式子
}
void handler(int d, int v, int a)
{
    // first 表示进入主干道后第一个碰到的测速仪(下标)
    int first = std::lower_bound(p.begin(), p.end(), d) - p.begin();
    // 进入主干道 ::: 没有遇到测速仪,跳过
    if (first >= m)
        return;
    // 匀速 ::: 那一开始不超速意味着永远不会超速
    if (a == 0)
    {
        if (v > V)
            seg.push_back({first, m - 1});
        return;
    }
    // 分类二分
    int l = first, r = m - 1, ans = -1, mid;
    // 减速,找到「最后一个」检测到超速的测速仪
    if (a < 0)
    {
        while (l <= r)
        {
            mid = l + r >> 1;
            if (speed(v, a, p[mid] - d) > V * V)
                ans = mid, l = mid + 1;
            else
                r = mid - 1;
        }
        if (ans != -1)
            seg.push_back({first, ans});
        return;
    }
    // 加速,找到「首个」能检测到加速的测速仪
    if (a > 0)
    {
        while (l <= r)
        {
            mid = l + r >> 1;
            if (speed(v, a, p[mid] - d) > V * V)
                ans = mid, r = mid - 1;
            else
                l = mid + 1;
        }
        if (ans != -1)
            seg.push_back({ans, m - 1});
    }
}
void solve()
{
    p.clear(); // T 组数组记得初始化
    seg.clear();
    std::cin >> n >> m >> L >> V;
    for (int i = 1; i <= n; i++)
        std::cin >> d[i] >> v[i] >> a[i]; // 起点, 初始速度, 加速度
    for (int i = 1, x; i <= m; i++)
        std::cin >> x, p.push_back(x); // pi 测试仪的位置
    for (int i = 1; i <= n; i++) // 处理出 ::: 每辆车 i 的测速仪超速的区间
        handler(d[i], v[i], a[i]);
    // 贪心 ::: 区间覆盖问题啦!
    sort(seg.begin(), seg.end());
    int last = -1, ans = 0;
    for (auto it : seg)
        if (last < p[it.l]) // 贪心的每次 ::: 选择不重合区间中的右端点
            last = p[it.r], ans++;
    std::cout << (int)seg.size() << " " << m - ans << "\n";
}
int main()
{
    std::ios::sync_with_stdio(false), std::cin.tie(nullptr);
    int T;
    std::cin >> T;
    while (T--)
        solve();
    return 0;
}
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号