1 条题解

  • 0
    @ 2025-11-11 8:21:48

    题意理解 我们有一个 N × M N×M 的网格,从 ( 1 , 1 ) (1,1) 出发,可以:

    从 ( x , y ) (x,y) 到 ( x , y + 1 ) (x,y+1),代价 x 2 x 2 (向右)

    从 ( x , y ) (x,y) 到 ( x + 1 , y ) (x+1,y),代价 c y c y ​ (向下)

    有 Q Q 次询问,求到 ( x i , y i ) (x i ​ ,y i ​ ) 的最小代价。

    N N 最大 10 9 10 9 ,不能直接 DP

    M M 最大 2 × 10 5 2×10 5 ,列数较少

    Q Q 最大 2 × 10 5 2×10 5

    初步分析 设 d p [ x ] [ y ] dp[x][y] 表示到 ( x , y ) (x,y) 的最小代价。

    转移:

    d p [ x ] [ y ]

    min ⁡ { d p [ x ] [ y − 1 ] + x 2 (从左边来) d p [ x − 1 ] [ y ] + c y (从上面来) dp[x][y]=min{ dp[x][y−1]+x 2

    dp[x−1][y]+c y ​

    (从左边来) (从上面来) ​

    边界: d p [ 1 ] [ 1 ]

    0 dp[1][1]=0。

    但 N N 太大,不能直接计算所有 x x。

    观察 对于固定的 y y,考虑 d p [ x ] [ y ] dp[x][y] 关于 x x 的变化。

    从 y − 1 y−1 到 y y 时,代价是 x 2 x 2 ,所以从不同 x x 在 y − 1 y−1 列转移到 y y 列时,代价是 d p [ x ] [ y − 1 ] + x 2 dp[x][y−1]+x 2 。

    从上面下来的代价是 d p [ x − 1 ] [ y ] + c y dp[x−1][y]+c y ​ 。

    这其实是一个 动态规划与凸壳优化 的经典形式。

    问题转化 我们考虑 从某一列到下一列 的转移。

    假设我们已经知道第 y − 1 y−1 列的所有 d p [ x ] [ y − 1 ] dp[x][y−1],那么在第 y y 列:

    d p [ x ] [ y ]

    min ⁡ ( d p [ x ] [ y − 1 ] + x 2 ,

    d p [ x − 1 ] [ y ] + c y ) dp[x][y]=min(dp[x][y−1]+x 2 , dp[x−1][y]+c y ​ ) 但注意:从上往下的转移是 单调的(只能向下走),所以我们可以按 x x 从小到大计算。

    更清晰的思路 定义 f y ( x )

    d p [ x ] [ y ] f y ​ (x)=dp[x][y]。

    那么:

    f y ( x )

    min ⁡ ( f y − 1 ( x ) + x 2 ,

    f y ( x − 1 ) + c y ) f y ​ (x)=min(f y−1 ​ (x)+x 2 , f y ​ (x−1)+c y ​ ) 并且 f 1 ( 1 )

    0 f 1 ​ (1)=0,对于 x

    1 x>1, f 1 ( x )

    ∞ f 1 ​ (x)=∞(因为第一列只能从(1,1)往下走)。

    关键观察 对于每个 y y, f y ( x ) f y ​ (x) 是一个 分段二次函数 的形式,并且是 凸函数(因为 x 2 x 2 是凸的,加上 min 和线性项保持凸性)。

    我们可以维护每个 y y 的 f y ( x ) f y ​ (x) 的 分段表示,每段是一个二次函数或线性函数。

    数学推导 假设在列 y y,我们有一些“决策点”,表示从列 y − 1 y−1 的某个 x x 直接向右走到当前列更优,而不是从上面下来。

    设 g y − 1 ( x )

    f y − 1 ( x ) + x 2 g y−1 ​ (x)=f y−1 ​ (x)+x 2 。

    那么:

    f y ( x )

    min ⁡ ( g y − 1 ( x ) ,

    f y ( x − 1 ) + c y ) f y ​ (x)=min(g y−1 ​ (x), f y ​ (x−1)+c y ​ ) 这个形式很像 斜率优化: 我们维护一个候选集合(即 g y − 1 ( x ) g y−1 ​ (x) 的凸包),然后与一个斜率为 c y c y ​ 的直线取 min。

    凸包维护 对于每个 y y,我们维护一个凸包(在 x x 空间),表示 g y − 1 ( x ) g y−1 ​ (x) 的下凸壳。

    g y − 1 ( x )

    f y − 1 ( x ) + x 2 g y−1 ​ (x)=f y−1 ​ (x)+x 2

    因为 f y − 1 ( x ) f y−1 ​ (x) 是凸的,加上 x 2 x 2 (凸)仍然是凸的,所以 g y − 1 ( x ) g y−1 ​ (x) 是凸的。

    因此它的最小值可以在凸包上二分查找。

    算法步骤 初始化: f 1 ( x ) f 1 ​ (x) 只在 x

    1 x=1 有定义,值为 0。

    对 y y 从 2 到 M M:

    从 y − 1 y−1 的凸包(表示 g y − 1 g y−1 ​ )得到当前列的最优起点

    计算新的一列 f y ( x ) f y ​ (x) 的凸包

    对每个询问 ( x i , y i ) (x i ​ ,y i ​ ),在 y i y i ​ 的凸包上二分找到 x i x i ​ 所在段,计算 f y i ( x i ) f y i ​

    ​ (x i ​ )。

    实现细节 我们不需要存所有 x x,只需要存凸包的 断点(即分段点)。

    对于每个 y y,我们维护:

    一个列表 segments,每个元素是 (start_x, a, b) 表示这一段 f y ( x )

    a x 2 + b x + c o n s t f y ​ (x)=ax 2 +bx+const? 其实更简单:因为从左边来的代价是 x 2 x 2 ,从上面来的代价是线性 c y c y ​ ,所以 f y ( x ) f y ​ (x) 是分段二次或线性。

    实际上,可以证明: f y ( x ) f y ​ (x) 是 分段二次函数,每段形式为 x 2 + α x + β x 2 +αx+β 或线性函数。

    简化方法(官方解法思路) 定义 h y ( x )

    f y ( x ) − x 2 h y ​ (x)=f y ​ (x)−x 2 。

    那么转移变为:

    h y ( x )

    min ⁡ ( h y − 1 ( x ) ,

    h y ( x − 1 ) + c y − ( 2 x − 1 ) ) h y ​ (x)=min(h y−1 ​ (x), h y ​ (x−1)+c y ​ −(2x−1)) 这里 2 x − 1 2x−1 来自 x 2 − ( x − 1 ) 2

    2 x − 1 x 2 −(x−1) 2 =2x−1。

    这样 h y ( x ) h y ​ (x) 是 分段线性 的,更容易用凸包维护。

    最终算法(用 h y ( x ) h y ​ (x)) 初始化: h 1 ( 1 )

    − 1 h 1 ​ (1)=−1(因为 f 1 ( 1 )

    0 f 1 ​ (1)=0,减去 1 2 1 2 得 -1),其它为无穷大。

    对 y y 从 2 到 M M:

    将 h y − 1 h y−1 ​ 的凸包传递过来

    在凸包上加一个斜率为 c y c y ​ 的直线,并调整截距(因为 c y − ( 2 x − 1 ) c y ​ −(2x−1) 中的 − 2 x −2x 会影响)

    实际上更简单:我们维护 h y ( x ) h y ​ (x) 的凸包(下凸壳),每次加入新的候选点 ( x , h y − 1 ( x ) ) (x,h y−1 ​ (x)),并与从左边来的路径取 min。

    具体实现时,用单调队列维护凸包点 ( x , v a l ) (x,val),其中 v a l

    h y ( x ) val=h y ​ (x)。

    查询 对于查询 ( x i , y i ) (x i ​ ,y i ​ ),在 y i y i ​ 的凸包中二分找到 x i x i ​ 所在的线段,用线段公式计算 h y i ( x i ) h y i ​

    ​ (x i ​ ),然后 f y i ( x i )

    h y i ( x i ) + x i 2 f y i ​

    ​ (x i ​ )=h y i ​

    ​ (x i ​ )+x i 2 ​ 。

    复杂度 每列凸包操作均摊 O ( 1 ) O(1)

    总 O ( M ) O(M) 构建

    每个查询 O ( log ⁡ M ) O(logM) 二分

    代码框架 cpp #include <bits/stdc++.h> using namespace std; using ll = long long;

    struct Point { ll x, val; };

    ll N, M, Q; vector c; vector<vector> hull; // hull[y] 存储凸包点

    ll query(ll x, ll y) { // 在 hull[y] 中二分找到 x 所在的线段 // 返回 h_y(x) + x*x }

    int main() { cin >> N >> M; c.resize(M + 1); for (ll i = 1; i <= M; i++) cin >> c[i];

    hull.resize(M + 1);
    // 初始化 y=1
    hull[1].push_back({1, -1});
    
    for (ll y = 2; y <= M; y++) {
        // 从 hull[y-1] 构建 hull[y]
        // 这里实现凸包合并与 min 操作
        // 略去具体实现
    }
    
    cin >> Q;
    while (Q--) {
        ll x, y;
        cin >> x >> y;
        cout << query(x, y) << "\n";
    }
    

    } 总结 这道题的核心是:

    将大规模网格 DP 转化为 凸包维护 问题

    利用 h y ( x )

    f y ( x ) − x 2 h y ​ (x)=f y ​ (x)−x 2 将代价转化为分段线性函数

    用单调队列维护凸包,支持快速查询

    最终答案 = h y ( x ) + x 2 h y ​ (x)+x 2

    这样就能在 O ( M + Q log ⁡ M ) O(M+QlogM) 时间内解决,适用于 N N 高达 10 9 10 9 的情况。

    • 1

    信息

    ID
    4902
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者