凸壳的常见维护方式及其优劣

我们知道,对于具有凸性的DP存在一种技巧叫 Slope Trick,一般指利用堆维护斜率拐点,但凸壳不止这一种表达方法,还有一些。

先来看看我们需要的操作:

  1. 加法:\(g(x)=f(x)+h(x)\)\(h_1(x)=C,h_2(x)=kx+b,h_3(x)=|x-a|\)

  2. 平移,然后取极值——这本质是闵可夫斯基和(负数下标就平移)

  3. 做闵可夫斯基和

    与一个较小的凸壳做卷积。

  4. 前后缀极值

    \(f'_x=\min_{y\le x}f_y\)

  5. \(f'_x=\min_{x-r\le y\le x-l}f_y\)

  6. 截断:

    \(f'_x=\min(f'_{x-1}+k,f_x)\)

然后有一些常用的方法:

  1. 维护斜率变化点

    Slope Trick 的经典办法,即维护所有的斜率转折点,也就是比如在 \(x>t\) 的所有斜线的斜率全部增加 \(1\),那么就插入一个 \(t\)实质上是斜率数组的差分的另一种表达(\(k_{i+1}-k_i\) 就是数字 \(i\) 出现次数)。一般用堆维护。

    丢失了具体的一个点信息,一般可以考虑维护图像的起点,以及首条斜线的斜率信息。

    然后就可以逐个还原出斜率数组进而还原图像。

    1. 加常函数就改起点,加 \(kx+b\) 可以看出来是给所有直线斜率加,那么打一个全体加标记,加绝对值函数相当于值 \(a\) 左侧斜率减一,右侧加一,插入两个 \(a\) 并更新维护的额外信息即可。

    2. 平移就给起点打标记。

    3. 做不了闵可夫斯基和。

      例如 \(f'_x=\min(f_{x},f_{x-1}+v_1,f_{x-2}+v_2)\)

      本质上是与 \(\lbrace 0,v_1,v_2\rbrace\) 做闵可夫斯基和。

    4. 相当于将斜率为正/负的段全部删掉,一般是用对顶堆维护 \(\le 0\)\(>0\) 的部分,这也更方便维护最值。

    5. 这是特殊的闵可夫斯基和

      做适当的平移后,相当于扩展了最值的位置。

      一般也考虑用对顶堆维护,但我们还需要额外增加变量来表示最值段的长度。

      变化点集合有整体的加减标记,可以打 tag。

      \(O(1)\) 的,这很重要

    6. 这样的操作本质上是在删除斜率 \(>k\) 的部分。

      弹出这些变化点即可。其实是上上个操作的简化版。

    优点:相当于维护斜率的差分数组,但是避免了显式维护,这样可维护的值域之类都变大了。

    缺点:做不了闵可夫斯基和,需要额外维护起点,最值相关信息。

  2. 维护斜率数组。

    一般考虑采用堆,mapset,平衡树,对顶堆,左偏树等有序且支持插入的结构。

    1. 加一次函数相当于区间加,加绝对值也差不多,但支持定义域限制。

      对于特定结构,例如对顶堆加 \(a=0\) 的绝对值函数也可以打标记做。

    2. 维护平移下标。

    3. 归并排序,或者直接将另一个凸壳并入有序结构。

      set,堆,平衡树等都能做。

    4. 相当于在斜率归零的前/后赋 \(0\)

      可以考虑平衡树维护,可以考虑对顶堆直接清空一侧。

    5. 当闵可夫斯基和做,\(O(r-l)\)复杂度较劣

    6. 区间赋值,二分到相应位置。

      只有 \(k=0\) 好做。这时候退化为 4.

      否则就涉及区间覆盖操作了,只能用平衡树之类的做。

    优点:可以很快速做闵可夫斯基和。

    缺点:所需长度要有限,维护复杂度较高,且维护操作比较复杂,涉及到区间操作一般就只能上平衡树了。

  3. 维护斜率变化总量。abc217H

    维护斜率变化点的更进一步形式,维护二元组 \((p,\Delta)\) 表示将斜率的差分数组 \(c_p\) 加上 \(\Delta\).

    基本可以照搬斜率变化点的方法,但是需要注意二元组的合并(表达插多个的意思)。

  4. 维护凸壳顶点。

    什么都能做。

    做不了加法,代价非常大。

  5. 平衡树暴力维护分段一次函数。

    直接存下每一段斜率对应的初始位置和结束位置,或者斜率变化点,用平衡树维护。

    重型武器,全都能做,但是常数很大,代码很长,风险高

  6. 维护分段函数

    有时候不止是一次函数,可能涉及高次函数,一般只能如此暴力处理。


附赠 AI 回答:

在 DP 优化和 Slope Trick 的语境下,“凸壳”指的是动态规划的状态函数 \(f(x)\) 的图像(因为它是一个凸函数)。

对凸壳的操作,本质上就是函数变换。我们需要根据这些数学变换,找出对应的数据结构(通常是双堆)的操作。

以下是常见的凸壳操作、几何含义及其实现方式:


1. 点加 (Pointwise Addition)

公式: \(g(x) = f(x) + h(x)\)

这是最基础的操作。根据 \(h(x)\) 的不同,处理方式也不同:

  • 加常数: \(h(x) = C\)
    • 含义:整个图像向上平移。
    • 操作min_val += C。不影响斜率和拐点,只影响记录的最小函数值。
  • 加一次函数: \(h(x) = kx + b\)
    • 含义:图像发生倾斜(旋转)。
    • 操作:这会改变所有线段的斜率。
      • 如果在数据结构中维护的是“绝对斜率”,则需要打全局标记。
      • 但在 Slope Trick 的双堆实现中,堆维护的是“斜率变化的位置(拐点)”。加线性函数不会改变拐点的 \(x\) 坐标,只会改变最底部的那个平坦段的斜率值(比如从 0 变成 \(k\))。通常通过调整记录“最小值位置”的逻辑来处理,或者忽略它(如果只需要求最终的最值)。
  • 加 V 型函数(核心): \(h(x) = |x - a|\)
    • 含义:在 \(x=a\) 处,左侧斜率 -1,右侧斜率 +1。
    • 操作:向左堆 \(L\) 插入 \(a\),向右堆 \(R\) 插入 \(a\)
    • 注:如果是加一般的凸函数,可以分解为多个 \(|x-a|\) 的叠加。

2. 坐标平移 (Translation / Shift)

公式: \(g(x) = f(x - c)\)

  • 含义:整个图像向右平移 \(c\) 个单位。
  • 几何:所有的拐点 \(x\) 坐标都变成了 \(x + c\)
  • 操作
    • 使用 Lazy Tag
    • 维护全局变量 lazy_Llazy_R
    • push 元素时,存入 val - lazy;当 poptop 时,读出 val + lazy
    • 操作:lazy_L += c; lazy_R += c;

3. 取前缀/后缀最小值 (Prefix/Suffix Min)

公式: \(g(x) = \min_{y \le x} f(y)\) (前缀最小)
公式: \(g(x) = \min_{y \ge x} f(y)\) (后缀最小)

  • 含义:将图像的一侧“拉平”。
    • 前缀最小:图像右侧斜率 \(>0\) 的部分全部变成斜率 \(=0\)
    • 后缀最小:图像左侧斜率 \(<0\) 的部分全部变成斜率 \(=0\)
  • 操作
    • 对于 \(\min_{y \le x}\)清空右堆 \(R\)(或者不断 pop 直到空)。此时函数在最低点及其右侧变成水平线。
    • 对于 \(\min_{y \ge x}\)清空左堆 \(L\)

4. 窗口最小值 / 扩展 (Sliding Window Min / Expansion)

公式: \(g(x) = \min_{x - r \le y \le x - l} f(y)\)
这等价于 \((\min, +)\) 卷积的一个特例:\(g(x) = \min_y (f(y) + h(x-y))\),其中 \(h(x)\) 是在区间 \([l, r]\) 取值 0,其他地方取 \(\infty\) 的函数。

  • 含义:这是第 2 点和第 3 点的结合。它把函数图像中最底部的“平底”部分拉宽了。
    • 最低点区间 \([min\_pos_L, min\_pos_R]\) 变成了 \([min\_pos_L + l, min\_pos_R + r]\)
    • 左边的拐点向右移 \(l\),右边的拐点向右移 \(r\)
  • 操作
    • lazy_L += l;
    • lazy_R += r;
    • 这个操作非常强大,常见于这就好比把 \(f(x)\) 的那个“碗底”给拽宽了。

5. \((\min, +)\) 卷积 (Infimal Convolution)

公式: \(g(x) = \min_{y} (f(y) + h(x-y))\)

  • 几何含义(Minkowski Sum)
    如果 \(f\)\(h\) 都是凸函数,这个操作等价于几何上的 闵可夫斯基和
    直观地说,就是把两个函数的线段按斜率大小归并排序重新拼起来。
    • 例如:\(f\) 有一段斜率为 1 长度为 2 的线段,\(h\) 有一段斜率为 1 长度为 3 的线段,那么 \(g\) 会有一段斜率为 1 长度为 5 的线段。
  • 常见特例
    如果 \(h(x) = k|x|\) (即斜率为 \(\pm k\)),这相当于限制了转移的“速度”。它会让 \(f(x)\) 图像中斜率绝对值 \(> k\) 的部分被“切掉”或者说被斜率 \(k\) 的直线替代。

6. 函数合并 (Function Merge)

公式: \(g(x) = f(x) + p(x)\) (其中 \(f\)\(p\) 都是维护好的复杂凸函数)

  • 场景:树上 DP,将子树的 DP 状态合并到父节点。
  • 几何含义:两个凸函数的叠加。其拐点集合基本上是两个原函数拐点集合的并集。
  • 操作
    • 启发式合并 (Small-to-Large):将较小的堆中的元素暴力 pop 出来 push 到较大的堆中。复杂度 \(O(N \log^2 N)\)
    • 可并堆 (Leftist Tree / Skew Heap):直接在 \(O(\log N)\) 时间内合并两个堆。这是处理 Tree DP + Slope Trick 的标准做法。

7. 限制定义域 (Domain Restriction)

公式: 强制 \(x \in [L, R]\),否则 \(f(x) = \infty\)

  • 几何含义:在 \(x=L\) 左侧和 \(x=R\) 右侧加上近乎垂直的“墙”。
  • 操作
    • 向左堆 \(L\) 中插入无穷多个 \(L\)(或者是需要的最大斜率数量)。
    • 向右堆 \(R\) 中插入无穷多个 \(R\)
    • 这会使得越过边界后的斜率迅速变得极大,从而在求最值时被排除。

8. 翻转 (Reflection)

公式: \(g(x) = f(-x)\)

  • 含义:图像沿 Y 轴翻转。
  • 操作
    • 交换左堆 \(L\) 和右堆 \(R\)
    • 堆内的值取相反数(因为 \(x\) 变为了 \(-x\))。
    • 这通常需要堆支持打“乘法标记”或者 Lazy Tag 配合交换指针。

总结表

数学操作 对应 DP 意义 Slope Trick (双堆) 操作
$+ x-a $
\(f(x-c)\) 转移代价:常数位移 lazy_L += c; lazy_R += c;
\(\min_{y \le x} f(y)\) 只要前面的最优解 清空右堆 \(R\) (或 R 只保留最小值点)
\(\min_{x-r \le y \le x-l} f(y)\) 滑动窗口转移 lazy_L += l; lazy_R += r;
\(f(x) + g(x)\) 树上子节点合并 启发式合并堆 或 使用左偏树
\(x \in [L, R]\) 加上边界墙 插入多个 \(L\) 到左堆,多个 \(R\) 到右堆

理解这些操作后,你就可以像搭积木一样,把复杂的 DP 方程分解成这些基本操作,然后用 Slope Trick 的模板“翻译”成代码。


在算法竞赛中,提到“DP 转移的凸壳维护”,通常有两种完全不同的场景。根据你的需求,可能指的是以下两种之一:

  1. Slope Trick 的进阶维护:维护一个分段线性凸函数 \(f(x)\)(如上一条回答所述),但在树上合并或进行复杂操作。
  2. 斜率优化(Convex Hull Trick, CHT):维护一堆直线 \(y = kx + b\),求某点 \(x\) 处的最大/最小值(即直线的下包络或上包络)。

以下是针对这两种场景的常见维护方式:


一、 针对直线集合的优化:斜率优化 (CHT)

这是最常见的“凸壳维护”,通常处理形如 \(DP[i] = \min_{j < i} \{ DP[j] + A[i] \times B[j] \} + C[i]\) 的转移方程。这类方程可以转化为直线形式 \(y = kx + b\)

1. 单调队列 (Deque) —— 静态/单调场景

  • 适用条件:斜率 \(k\) 单调,查询坐标 \(x\) 单调。
  • 原理
    • 因为斜率单调,凸壳上的直线按顺序排列。
    • 因为查询单调,最优解只会向一个方向移动。
    • 使用双端队列维护凸壳上的点(直线交点)。队首用于弹出过时的最优解,队尾用于加入新直线并维护凸性(利用叉积或斜率比较)。
  • 复杂度\(O(N)\),每个元素进出队列一次。

2. 二分查找 (Binary Search on Stack) —— 斜率单调,查询不单调

  • 适用条件:插入直线的斜率 \(k\) 是单调的,但查询的 \(x\) 是无序的。
  • 原理
    • 使用单调栈(或 std::vector)维护凸壳。
    • 由于查询不单调,不能像 Deque 那样直接弹队首。需要在栈上进行二分查找,找到当前 \(x\) 对应的最优直线。
  • 复杂度\(O(N \log N)\)

3. 李超线段树 (Li Chao Tree) —— 动态场景的首选

  • 适用条件:斜率 \(k\) 无序,查询 \(x\) 无序,甚至支持区间插入直线。
  • 原理
    • 这是一种特殊的线段树。每个节点存储一条“优势直线”(在该区间中点处最优的直线)。
    • 插入新直线时,通过比较中点的值,将劣势直线“下放”到左右子树递归处理。
    • 查询时,从根走到叶子,取路径上所有直线在 \(x\) 处的最大/最小值。
  • 优点:代码比平衡树好写,常数小,支持区间加直线。
  • 复杂度\(O(N \log (\text{值域}))\)

4. 动态凸包 (Dynamic Convex Hull using std::set)

  • 适用条件:全动态(斜率、查询均无序),且值域很大不适合李超树。
  • 原理
    • 利用 std::set 按照斜率排序存储直线。
    • 维护每条直线与相邻直线的交点(有效区间)。
    • 插入时,利用 iterator 检查与左右邻居的凸性,删除被覆盖的直线。
    • 这个技巧常被称为 "LineContainer"(源自 KACTL 模板库)。
  • 复杂度\(O(N \log N)\)

二、 针对分段线性函数的优化:Slope Trick 进阶

如果你指的依然是上一条回答中的那种“分段函数”维护(Slope Trick),除了用两个优先队列(堆)之外,还有以下方式应对更复杂的操作:

1. 可并堆 (Mergeable Heaps)

  • 场景树上 DP。需要在树的节点处合并子树的 DP 状态(即合并两个凸函数 \(f(x) + g(x)\))。
  • 数据结构:左偏树 (Leftist Tree) 或 斜堆 (Skew Heap)。
  • 原理
    • Slope Trick 本质是维护拐点集合。
    • 两个凸函数相加,等于它们的拐点集合的并集
    • 使用 std::priority_queue 合并是 \(O(N)\) 的(启发式合并是 \(O(N \log^2 N)\)),而使用左偏树可以将合并优化到 \(O(\log N)\)

2. 平衡树 (Treap / Splay)

  • 场景:不仅仅是 \(f(x) + |x-a|\),还涉及区间平移、区间取 \(\min\)、翻转、或者查询特定区间的函数值。
  • 原理
    • 将拐点存储在平衡树中。
    • 每个节点可以维护子树的斜率增量和、截距等信息。
    • 可以通过 Lazy Tag 实现函数的区间平移 \((x \to x+c)\) 或值的平移 \((y \to y+c)\)
  • 复杂度\(O(N \log N)\),功能最强但也最难写。

3. 线段树 (Segment Tree) 维护差分

  • 场景:如果 \(x\) 的范围比较小,可以直接在线段树上维护函数的斜率。
  • 原理
    • 凸函数的二阶导(斜率的变化率)是非负的。
    • 线段树维护每个位置的斜率值(或斜率的变化量)。
    • \(|x-a|\) 等价于区间修改斜率。
    • 查询最小值等价于在线段树上找斜率为 0 的位置。

三、 其他相关优化技巧

除了上述几何结构的直接维护,还有两类技巧常用于凸性相关的 DP:

1. WQS 二分 (Aliens Trick / 带权二分)

  • 问题类型:在凸函数上求“恰好选 \(k\) 个物品”的最优解,或者有次数限制的问题。
  • 原理
    • 如果 DP 状态 \((k, \text{cost})\) 构成的函数是凸的。
    • 我们可以把限制 \(k\) 去掉,改为每选一次物品付出额外代价 \(\lambda\)(Lagrange Multiplier)。
    • 二分这个代价 \(\lambda\),使得最优解恰好选了 \(k\) 次。
  • 效果:通常将 \(O(N^2)\)\(O(NK)\) 优化为 \(O(N \log (\text{值域}))\)

2. 决策单调性分治 (Divide & Conquer Optimization)

  • 问题类型\(DP[i] = \min_{j < i} \{ DP[j] + w(j, i) \}\),其中 \(w\) 满足四边形不等式。
  • 原理
    • 不需要显式维护凸壳。
    • 利用决策点 \(opt[i]\) 单调递增的性质 (\(opt[i] \le opt[i+1]\))。
    • 通过分治法,先求出区间中点的最优决策,然后递归处理左右两边。
  • 复杂度\(O(N \log N)\)

总结建议

  • 如果是直线求最值(如 \(ax+b\)):
    • 简单/离线:李超线段树(首选,好写)。
    • 必须在线且值域巨大:std::set 维护动态凸包
  • 如果是V型/碗型函数维护(如 \(|x-a|\)):
    • 序列问题:对顶堆 (Priority Queue)
    • 树上合并问题:左偏树 (Leftist Tree)
  • 如果是限制次数 \(K\)
    • WQS 二分
posted @ 2026-01-27 20:55  spdarkle  阅读(29)  评论(0)    收藏  举报