1 条题解

  • 0
    @ 2025-10-25 20:01:59

    一、问题分析与数学建模

    1.1 问题本质

    这是一道斜率优化DP + 贪心的经典问题,核心在于如何高效地处理二次函数形式的状态转移。

    问题形式化

    给定:

    • nn 个站点,mm 班列车
    • 列车 ii:从站点 xix_i 在时刻 pip_i 出发,到站点 yiy_i 在时刻 qiq_i 到达
    • 等待时间 tt 的代价:At2+Bt+CAt^2 + Bt + C
    • 到达 nn 的额外代价:到达时刻 zz

    目标:从站点 11 出发(时刻 00),到达站点 nn,最小化总烦躁值。

    烦躁值公式

    $$\text{Cost} = q_{s_k} + (Ap_{s_1}^2 + Bp_{s_1} + C) + \sum_{j=1}^{k-1} \left[A(p_{s_{j+1}} - q_{s_j})^2 + B(p_{s_{j+1}} - q_{s_j}) + C\right] $$

    1.2 核心数学观察

    观察 1:DP 状态设计

    定义 f[i]f[i]:乘坐第 ii 班列车时的最小烦躁值(不包括到达时间 qiq_i)。

    初始状态:虚拟一个编号为 00 的"列车",表示在站点 11 等待。

    • x0=1,y0=1,p0=0,q0=0x_0 = 1, y_0 = 1, p_0 = 0, q_0 = 0
    • f[0]=0f[0] = 0

    答案minyi=n{f[i]+qi}\min_{y_i = n} \{f[i] + q_i\}


    观察 2:朴素 DP 转移

    对于列车 ii,可以从满足以下条件的列车 jj 转移:

    • yj=xiy_j = x_i(站点连续)
    • qjpiq_j \le p_i(时间可行)

    朴素转移方程

    $$f[i] = \min_{j \in S_i} \left\{ f[j] + A(p_i - q_j)^2 + B(p_i - q_j) + C \right\} $$

    其中 Si={j:yj=xiqjpi}S_i = \{j : y_j = x_i \land q_j \le p_i\}

    时间复杂度O(m2)O(m^2),对于 m2×105m \le 2 \times 10^5 不可接受。


    观察 3:转移方程的展开与变形

    展开转移方程:

    $$\begin{aligned} f[i] &= f[j] + A(p_i - q_j)^2 + B(p_i - q_j) + C \\ &= f[j] + Ap_i^2 - 2Ap_iq_j + Aq_j^2 + Bp_i - Bq_j + C \end{aligned} $$

    整理得:

    $$f[i] = (Ap_i^2 + Bp_i + C) + \left[f[j] + Aq_j^2 - Bq_j - 2Ap_iq_j\right] $$

    关键变换:将与 jj 有关的项分离出来。

    设:

    • X(j)=qjX(j) = q_j(横坐标)
    • Y(j)=f[j]+Aqj2BqjY(j) = f[j] + Aq_j^2 - Bq_j(纵坐标)

    则:

    $$f[i] = (Ap_i^2 + Bp_i + C) + Y(j) - 2Ap_i \cdot X(j) $$

    要最小化 f[i]f[i],等价于最小化:

    Y(j)2ApiX(j)Y(j) - 2Ap_i \cdot X(j)

    观察 4:几何意义

    将每个决策点 jj 看作平面上的点 (X(j),Y(j))(X(j), Y(j))

    要最小化 Y(j)2ApiX(j)Y(j) - 2Ap_i \cdot X(j),等价于找一条斜率为 k=2Apik = 2Ap_i 的直线:

    y=kx+by = kx + b

    使得该直线经过某个决策点,并且截距 bb 最小

    几何解释

    • (X,Y)(X, Y) 坐标系中,决策点形成一个点集
    • 我们要找一条斜率为 kk 的直线,从下方切过这些点
    • 第一个接触到的点就是最优决策点

    观察 5:凸包优化

    引理 1:对于三个决策点 i,j,ki, j, k(按 XX 坐标递增),如果:

    $$\frac{Y(j) - Y(i)}{X(j) - X(i)} \ge \frac{Y(k) - Y(j)}{X(k) - X(j)} $$

    则决策点 jj 永远不是最优决策(被 iikk 支配)。

    证明

    • 左侧是 i,ji, j 连线的斜率
    • 右侧是 j,kj, k 连线的斜率
    • 如果左斜率 \ge 右斜率,说明 jji,ki, k 连线的上方或上面
    • 对于任意查询斜率 kk'
      • 如果 k<k' < 左斜率,选 ii 更优
      • 如果 k>k' > 右斜率,选 kk 更优
      • 如果左斜率 k\le k' \le 右斜率,由于左斜率 \ge 右斜率,这个区间为空或选 iikk 都不差
    • 因此 jj 永远不会被选中

    维护下凸包:保留满足斜率递增的决策点序列。


    1.3 算法框架

    阶段 1:预处理

    • 按列车出发时间 pip_i 排序
    • 初始化每个站点的决策点队列

    阶段 2:动态规划

    • 按排序后的顺序处理每班列车
    • 对于列车 ii
      1. 将已经到达 xix_iqjpiq_j \le p_i 的决策点加入凸包
      2. 在凸包上查询斜率为 2Api2Ap_i 的最优决策点
      3. 计算 f[i]f[i]
      4. ii 加入到站点 yiy_i 的待处理队列

    阶段 3:统计答案

    • 遍历所有到达站点 nn 的列车
    • min{f[i]+qi}\min\{f[i] + q_i\}

    二、斜率优化DP 详解

    2.1 凸包维护

    数据结构:对于每个站点 ss,维护一个双端队列 que[s]\texttt{que}[s]

    性质

    • 队列中的决策点按 XX 坐标(即 qjq_j)递增
    • 相邻决策点连线的斜率递增(下凸包)

    插入操作

    当新决策点 pos 到达时:
        while 队列长度 >= 2:
            取队尾两个点 i, j
            if 斜率(i,j) >= 斜率(j,pos):
                弹出 j(被 pos 支配)
            else:
                break
        将 pos 加入队尾
    

    查询操作

    给定查询斜率 k = 2*A*p[i]:
        while 队列长度 >= 2:
            取队头两个点 i, j
            if 斜率(i,j) <= k:
                弹出 i(j 更优)
            else:
                break
        返回队头元素
    

    2.2 斜率比较函数

    函数 1cmpk(i, j, k) - 判断是否删除 jj

    bool cmpk(int i, int j, int k) {
        return (Y(j) - Y(i)) * (X(k) - X(j)) >= (Y(k) - Y(j)) * (X(j) - X(i));
    }
    

    数学含义

    $$\frac{Y(j) - Y(i)}{X(j) - X(i)} \ge \frac{Y(k) - Y(j)}{X(k) - X(j)} $$

    即:斜率(i,j)(i,j) \ge 斜率(j,k)(j,k),则删除 jj

    注意:用乘法避免除法(防止精度问题和除零)。


    函数 2cmpx(i, j, val) - 判断是否删除 ii

    bool cmpx(int i, int j, int val) {
        return Y(j) - Y(i) <= val * (X(j) - X(i));
    }
    

    数学含义

    Y(j)Y(i)X(j)X(i)val\frac{Y(j) - Y(i)}{X(j) - X(i)} \le \text{val}

    即:斜率(i,j)(i,j) \le 查询斜率 val=2Api\text{val} = 2Ap_i,则删除 iijj 更优)。


    2.3 时间顺序处理

    为什么按 pip_i 排序?

    引理 2:按 pip_i 递增顺序处理列车,查询斜率 k=2Apik = 2Ap_i 单调递增。

    证明

    • 查询斜率 ki=2Apik_i = 2Ap_i
    • 如果 pi<pjp_i < p_j,则 ki<kjk_i < k_j(假设 A>0A > 0
    • 这保证了查询斜率单调,可以用单调队列优化

    注意:当 A=0A = 0 时,斜率恒为 00,退化为普通 DP。


    三、代码模块详解

    模块 1:全局变量与数据结构

    const int N = 1e5 + 10, M = 2e5 + 10;  // 注意:M应该是2e5而不是1e6
    int n, m, A, B, C;
    int x[M], y[M], p[M], q[M];  // 列车信息
    int ind[N];                   // 每个站点作为到达站的列车数量
    int id[M];                    // 列车按p[i]排序后的编号
    

    优先队列节点

    struct node {
        int x;  // 列车编号
        friend bool operator <(node x, node y) {
            return q[x.x] > q[y.x];  // 小根堆,按q[j]递增
        }
    };
    priority_queue<node> hp[N];  // 每个站点的待处理决策点
    

    凸包维护

    vector<int> que[N];     // 每个站点的决策点队列
    int head[N], tail[N];   // 队列的头尾指针
    int f[M];               // DP数组
    

    模块 2:读入与预处理

    cin >> n >> m >> A >> B >> C;
    
    for (int i = 1; i <= m; i++) {
        cin >> x[i] >> y[i] >> p[i] >> q[i];
        ++ind[y[i]];  // 统计到达每个站点的列车数
    }
    
    // 按出发时间排序
    iota(id + 1, id + 1 + m, 1);  // id数组初始化为1,2,...,m
    sort(id + 1, id + 1 + m, [&](int x, int y) {
        return p[x] < p[y];
    });
    
    // 初始化队列
    for (int i = 1; i <= n; i++) {
        que[i].resize(ind[i] + 3);  // 预分配空间
        head[i] = 1, tail[i] = 0;   // 空队列
    }
    
    que[1][++tail[1]] = 0;  // 站点1加入虚拟决策点0(f[0]=0, q[0]=0)
    

    关键点

    • iota 初始化排列
    • ind[y[i]] 用于预分配 vector 大小,避免动态扩容
    • 虚拟决策点 00 表示在站点 11 从时刻 00 开始等待

    模块 3:斜率优化函数

    int X(int j) {
        return q[j];
    }
    
    int Y(int j) {
        return f[j] + A * q[j] * q[j] - B * q[j];
    }
    

    定义坐标映射

    • X(j)=qjX(j) = q_j
    • Y(j)=f[j]+Aqj2BqjY(j) = f[j] + Aq_j^2 - Bq_j

    bool cmpk(int i, int j, int k) {
        return (Y(j) - Y(i)) * (X(k) - X(j)) >= (Y(k) - Y(j)) * (X(j) - X(i));
    }
    

    判断斜率关系

    $$\frac{Y(j) - Y(i)}{X(j) - X(i)} \ge \frac{Y(k) - Y(j)}{X(k) - X(j)} $$

    返回 true 表示 jj 应该被删除(不在下凸包上)。


    bool cmpx(int i, int j, int val) {
        return Y(j) - Y(i) <= val * (X(j) - X(i));
    }
    

    判断查询斜率

    Y(j)Y(i)X(j)X(i)val\frac{Y(j) - Y(i)}{X(j) - X(i)} \le \text{val}

    返回 true 表示 ii 应该被删除(jj 更优)。


    模块 4:DP 主循环

    for (int _ = 1; _ <= m; _++) {
        int i = id[_];  // 按出发时间顺序处理
        
        // 步骤1: 将满足条件的决策点加入凸包
        while (!hp[x[i]].empty() && q[hp[x[i]].top().x] <= p[i]) {
            int pos = hp[x[i]].top().x;
            
            // 维护下凸包:删除被支配的点
            while (head[x[i]] < tail[x[i]] && 
                   cmpk(que[x[i]][tail[x[i]] - 1], que[x[i]][tail[x[i]]], pos)) {
                --tail[x[i]];
            }
            
            que[x[i]][++tail[x[i]]] = pos;
            hp[x[i]].pop();
        }
        
        // 步骤2: 在凸包上查询最优决策点
        while (head[x[i]] < tail[x[i]] && 
               cmpx(que[x[i]][head[x[i]]], que[x[i]][head[x[i]] + 1], 2 * A * p[i])) {
            ++head[x[i]];
        }
        
        // 步骤3: 无可行转移
        if (head[x[i]] > tail[x[i]]) {
            f[i] = 1e18;
            continue;
        }
        
        // 步骤4: 计算f[i]
        int j = que[x[i]][head[x[i]]];
        int del = p[i] - q[j];
        f[i] = f[j] + A * del * del + B * del + C;
        
        // 步骤5: 将i加入到达站点y[i]的待处理队列
        hp[y[i]].push({i});
    }
    

    算法流程

    步骤 1:插入决策点

    • 从优先队列中取出所有 qjpiq_j \le p_i 的决策点
    • qjq_j 递增顺序插入凸包
    • 维护凸包性质(删除被支配的点)

    步骤 2:查询最优决策点

    • 查询斜率为 k=2Apik = 2Ap_i 的最优决策
    • 从队头删除不优的决策点

    步骤 3:处理无解情况

    • 如果队列为空,说明无法从站点 xix_i 出发
    • f[i]=f[i] = \infty

    步骤 4:计算 f[i]f[i]

    • 取队头元素 jj 作为最优决策
    • 计算等待时间 Δ=piqj\Delta = p_i - q_j
    • 转移:f[i]=f[j]+AΔ2+BΔ+Cf[i] = f[j] + A\Delta^2 + B\Delta + C

    步骤 5:延迟插入

    • ii 加入到达站点 yiy_i 的优先队列
    • 等待后续从 yiy_i 出发的列车处理时再插入凸包

    模块 5:统计答案

    int ans = 1e18;
    
    for (int i = 1; i <= m; i++) {
        if (y[i] == n) {
            ans = min(ans, f[i] + q[i]);
        }
    }
    
    cout << ans << '\n';
    

    逻辑

    • 遍历所有到达站点 nn 的列车
    • f[i]f[i] 是不包括到达时间的烦躁值
    • 总烦躁值 =f[i]+qi= f[i] + q_i(到达时刻的额外代价)

    四、正确性证明

    4.1 DP 转移的正确性

    定理 1:状态 f[i]f[i] 正确表示乘坐第 ii 班列车时的最小烦躁值(不含到达时间)。

    证明(归纳法):

    基础f[0]=0f[0] = 0(虚拟起点),正确。

    归纳:假设对所有 j<ij < i(按处理顺序),f[j]f[j] 正确。

    对于 ii,由于按 pip_i 递增顺序处理,所有可能转移到 iijj 都已经处理过。

    转移方程:

    $$f[i] = \min_{j \in S_i} \{f[j] + A(p_i - q_j)^2 + B(p_i - q_j) + C\} $$

    由归纳假设,f[j]f[j] 正确,因此 f[i]f[i] 正确。


    4.2 斜率优化的正确性

    定理 2:凸包维护算法正确找到最优决策点。

    证明

    性质 1:队列中决策点按 XX 坐标(qjq_j)递增。

    性质 2:相邻决策点连线斜率递增(下凸包)。

    引理 3:给定查询斜率 kk,最优决策点是使得连线斜率首次 >k> k 的前一个点。

    证明

    • 设队列为 p1,p2,,ptp_1, p_2, \ldots, p_t
    • 斜率递增:k1<k2<<kt1k_1 < k_2 < \cdots < k_{t-1}(其中 kik_ipip_ipi+1p_{i+1} 的斜率)
    • 如果 ki1k<kik_{i-1} \le k < k_i,则 pip_i 是最优决策
    • 查询算法通过从队头删除 kjkk_j \le k 的点,正确找到 pip_i

    4.3 时间顺序的必要性

    定理 3:必须按 pip_i 递增顺序处理列车。

    证明

    反例:如果不排序,可能出现 pi<pjp_i < p_jiijj 之后处理。

    此时处理 jj 时,ii 还未加入决策集合,可能错过最优转移。

    正面论证

    • pip_i 排序后,处理 ii 时,所有 qjpiq_j \le p_ijj 都已处理
    • 保证转移的完备性

    五、复杂度分析

    5.1 时间复杂度

    预处理

    • 排序:O(mlogm)O(m \log m)
    • 初始化:O(n+m)O(n + m)

    DP 主循环

    • 外层循环:O(m)O(m)
    • 对于每个列车 ii
      • 插入决策点:每个决策点最多入队出队一次,均摊 O(1)O(1)
      • 查询最优决策:每个决策点最多从队头弹出一次,均摊 O(1)O(1)
      • 计算转移:O(1)O(1)

    总时间复杂度O(mlogm+m)=O(mlogm)O(m \log m + m) = O(m \log m)

    对于 m2×105m \le 2 \times 10^5,约为 3.6×1063.6 \times 10^6 次操作,完全可行。


    5.2 空间复杂度

    • 列车信息:O(m)O(m)
    • DP 数组:O(m)O(m)
    • 决策点队列:O(n×m)O(n \times m)(最坏情况,实际远小于此)
    • 优先队列:O(m)O(m)

    总空间复杂度O(n+m)O(n + m)(均摊)


    5.3 优化空间

    问题:原代码 const int M = 1e6 + 10 过大。

    优化:改为 const int M = 2e5 + 10

    原因

    • m2×105m \le 2 \times 10^5
    • 10610^6 的数组浪费空间,可能导致栈溢出(Segmentation Fault)

    六、算法优化与扩展

    6.1 特殊情况的优化

    情况 1A=B=C=0A = B = C = 0

    • 烦躁值只有到达时间
    • 退化为最短路问题,用 Dijkstra 或 BFS

    情况 2A=0A = 0

    • 转移方程变为线性:f[i]=f[j]+B(piqj)+Cf[i] = f[j] + B(p_i - q_j) + C
    • 仍需斜率优化(斜率为 BB

    情况 3xi<yix_i < y_i(单向递增)

    • 可以按站点分层处理
    • 简化转移逻辑

    6.2 代码健壮性改进

    改进 1:数组大小检查

    const int M = 2e5 + 10;  // 从1e6改为2e5
    

    改进 2:边界检查

    if (head[x[i]] > tail[x[i]]) {
        f[i] = 1e18;
        continue;
    }
    

    改进 3:调试输出(可选)

    #ifdef DEBUG
        cerr << "Processing train " << i << ": " << x[i] << "->" << y[i] 
             << " at time " << p[i] << "->" << q[i] << endl;
    #endif
    

    6.3 相关问题与扩展

    1. 变种问题

      • 多起点多终点
      • 动态添加/删除列车
      • 不同的代价函数
    2. 一般化

      • 高维凸包维护
      • 在线查询(持久化数据结构)
    3. 理论延伸

      • 凸包优化的其他应用
      • 斜率优化DP的变体

    七、知识点总结

    7.1 核心算法技巧

    1. 斜率优化DP

      • 将二次DP转化为凸包问题
      • 维护下凸包
    2. 单调队列

      • 维护决策点凸包
      • 均摊 O(1)O(1) 插入删除
    3. 贪心排序

      • 按时间顺序处理
      • 保证单调性
    4. 几何转化

      • DP问题转化为几何问题
      • 利用几何性质优化
    5. 优先队列

      • 延迟插入决策点
      • 维护时间顺序

    八、总结

    8.1 算法精髓

    本题采用的斜率优化DP算法的核心思想:

    1. DP建模:定义状态 f[i]f[i] 表示乘坐第 ii 班列车的最小代价
    2. 数学变形:将二次转移方程转化为几何形式
    3. 凸包维护:用单调队列维护决策点的下凸包
    4. 单调性利用:按时间排序,利用查询斜率单调性优化
    5. 延迟插入:用优先队列管理决策点的插入时机
    • 1

    信息

    ID
    4106
    时间
    1000ms
    内存
    512MiB
    难度
    9
    标签
    递交数
    2
    已通过
    1
    上传者