• DP 优化浅谈


    前言

    DP 所要解决的是多阶段决策问题,它利用递归的思想,将规模为 \(n\) 的问题转化为规模较小的问题,直到转化为小到能够直接求解的子问题。通常来说这样做时间复杂度是指数级的,但是如果所有不同的子问题的数目是多项式级别,那么多项式时间就可以解决这个问题,这就是 DP 的本质。

    DP 有三个要素:

    1. 所有不同的子问题组成的表。
    2. 问题解决的依赖关系,这可以看成是一个图。
    3. 填充子问题表的顺序(实际上是这个图的一个拓扑排序)。

    如果状态数量为 \(O(n^t)\),转移需要依赖于其它 \(O(n^e)\) 个子问题,那么称这个问题是 \(tD/eD\) 的。

    这样可以得到四类典型的动态规划方程:

    • \(1D/1D\):已知 \(D_0\),方程为 \(E_j=\min(D_i+w(i,j))\)
    • \(2D/0D\):已知 \(D_{i,0},D_{0,j}\),方程为 \(E_{i,j}=\min(D_{i-1,j}+x_i,d_{i,j-1}+y_i,D_{i-1,j-1}+z_{i,j})\)
    • \(2D/1D\):已知 \(D_{i,i}=0\),方程为 \(E_{i,j}=w(i,j)+\min_{k=i+1}^j(D_{i,k-1}+D_{k,j})\)
    • \(2D/2D\):已知 \(D_{i,0},D_{0,j}\),方程为 \(E_{i,j}=\min(D_{i',j'}+w(i'+j',i+j))(0\le i'<i,0\le j'<j)\)

    DP 问题最直接的算法是 \(O(n^{e+t})\) 的,但我们不满足与此,于是产生了对应的优化手段。

    DP 优化无外乎状态优化和转移优化两种手段,状态优化往往依赖于具体题目而变,我们主要研究的是转移优化,转移优化常见的手段有:斜率优化,四边形不等式优化和具体数据结构优化等等,还有一类特殊的针对递推线性的优化即矩阵乘法。

    矩阵快速幂优化

    主要是利用矩阵快速幂来优化一些线性递推问题。高维 DP 如果某一维状态较少也可以用矩乘优化。

    这个比较普及不细讲,但是注意图的邻接矩阵也是可以用来矩阵加速的。

    例题:

    前缀和优化

    当转移碰到类似于 \(f_i=\sum_{l\le j\le r}g_i\) 时,我们可以对 \(g\) 做一个前缀和,之后两端点差分即可。同时,其中求和式可换成 \(\min\)\(\max\),不过优化的适用条件更为苛刻。

    另外多维 DP 有时也能用到前缀和优化,关键就是找到转移式有哪一个部分是只随着一个枚举变量变化而变化的,通过交换枚举顺序可以做到用前缀和优化。另外,要注意取的究竟是“前缀”还是“后缀”或是中间某两个端点,计算端点的值的时候要考虑交换 for 对循环范围的影响。【HEOI 2013】SAO 这题可能会让你对这部分有充分了解。

    例题:

    Two-pointer 优化

    最常见的双指针优化是“尺取法”。双指针优化,大概就是一个端点移动时,取最优值时另一个端点是单调移动的。

    例如,我想在一个有序数组 \(a\) 中找到两个数 \(a_i,a_j(i<j)\),使得 \(a_j-a_u\le C\)\(a_j-a_i\) 最大。那么我们移动左端点 \(i\) 时,只需找到 \(a_j\le C+a_i\) 的最大值即可。因为数组单调,\(j\) 也可以单调移动。当然根据题目不同,指针移动的方向是不同的,但是本质上就是固定其中一个端点,最优性地移动另一个端点。

    这样我们就能省去其中一维的枚举,起到优化复杂度的目的。

    例题

    决策单调性对一类 \(1D/1D\) DP 的优化

    而决策单调性是指,对于两个决策点 \(j\)\(k(j<k)\),如果对于状态 \(i\),决策 \(k\) 优于 \(j\),那么对于 \(\forall i'(i'>i)\) 都有 \(k\) 优于 \(j\),即从 \(k\) 转移一定更优,\(j\) 一定不再会成为最优决策,决策是单调的,和上文提到的双指针优化有点类似。

    未经优化的 \(1D/1D\) DP 通常复杂度是 \(O(n^2)\)。这部分将围绕 \(w(i,j)\) 不同的性质分情况来讨论不同的优化方法。注意多维 DP 中某一维可能也是 \(1D/1D\) 的,那么这一维也是可以单独拿出优化的,因此下面只讨论纯粹的 \(1D/1D\) DP。

    在这里,笔者叙述的思路是先讨论几个常见的优化方法,之后再引出其共同点以及一些拓展。内容有一定连续性,不建议跳跃阅读。

    \(w(i,j)\) 只含 \(i\)\(j\) 的项:单调队列优化

    我们不妨将转移写成 \(f_i=\min(\max){f_j+a_i+b_j}\),以 \(\min\) 为例。

    因为 \(a_i\)\(j\) 无关,实际上转移就是 \(f_i=\min(f_j+b_j)+a_i\)。那么我们只要找到 \(\min\) 括号内最小的即可。

    \(1\le j<i\),请移步前缀和优化。而一般来说 \(j\) 都是在一个移动的区间取值的,并且随 \(i\) 移动而移动,我们设为 \([l_i,r_i]\)\(l_i,r_i\) 单调不减。

    那么我们就只需维护一个单调递增的单调队列,单调队列中存放 \(f_j\),并且对应的下标也单调递增。对于每一个 \(i\),我们先将新增区间部分从队尾加入单调队列,因为需要维护单调性,如果尾部的元素大于需要加入的元素直接舍弃,因为它不可能成为最优决策点。接着考虑队首元素是否在可转移区间内,若不满足就一直出队直到找到一个满足条件的队首。那么此时的队首是 \([l_i,r_i]\) 中的最小值,更新 \(f_i\)

    例题:

    决策单调性适用的原理:四边形不等式与决策单调性

    我们先对决策单调性这一整个部分进行归纳总结。

    四边形不等式:对于任意 \(a\le b\le c\le d\),有 \(w(a,d)+w(b,c)\ge w(a,c)+w(b,d)\)

    注意,四边形不等式不同于代数上的不等式。它只是一些 \(w\) 的二元函数具备的某种特殊性质。

    定理:若 \(w(i,j)\) 满足四边形不等式,那么 \(1D/1D\) DP 是可以用决策单调性优化的。

    实际上某些情况下不等号是 \(\le\) 而不是 \(\ge\)。并且这两者适用的 DP 以及单调性是有些许差别的,具体地:

    • 当四边形不等式不等号取 \(\ge\) 时,若转移方程是取 \(\min\) 值,那么最优决策点与 \(i\) 的移动方向相,并且单调;
    • 当四边形不等式不等号取 \(\ge\) 时,若转移方程是取 \(\max\) 值,那么最优决策点与 \(i\) 的移动方向相,并且单调;
    • 当四边形不等式不等号取 \(\le\) 时,若转移方程是取 \(\min\) 值,那么最优决策点与 \(i\) 的移动方向相,并且单调;
    • 当四边形不等式不等号取 \(\le\) 时,若转移方程是取 \(\max\) 值,那么最优决策点与 \(i\) 的移动方向相,并且单调。

    其实本质上就是使得 \(w\) 有四边形不等式的性质。

    因为内容编排的原因,至此给出决策单调性斜率优化的例题。

    例题:

    不满足决策单调性的斜率优化

    在上文,我们提到过基于决策单调性利用斜率对 DP 进行优化的方法。其大致思想是,通过把 DP 转移方程进行变形,再把每一个转移点抽象成二维平面上的点。用线性规划去求对应 DP 值的最值。这样就可以贪心的去取上(或下)凸包上的点得出答案。由于这一过程遵循以下两个单调性:

    1. 随着 \(i\) 往后枚举,平面上新加入的点均出现在已有的点集的右(或左)侧,这样新加入的点一定会在凸包上。因此可以比较加入的点和凸包边缘的点的关系,维护凸包。整个过程复杂度可以做到 \(O(n)\)
    2. 随着 \(i\) 往后枚举,\(i\) 这一点对应的线性规划的直线的斜率是单调的。同时单调数据结构维护的凸包上的点两两相邻的斜率也是单调的,因此如果队首的点不再是 \(i\) 点的最优决策点的话,那么可以删掉队首,因为队首不再可能是之后的最优决策点。同样,这个过程复杂度也为 \(O(n)\)

    如果不满足上述两点,那么就不能用决策单调性 \(O(n)\) 进行优化了。为了解决这一问题,我们分别考虑下述情形。

    直线斜率不单调——二分凸包

    由于加入的点的横坐标依旧是满足单调性的,因此,维护凸包这一操作可以沿用上述操作。唯一需要考虑的就是如何在凸包上找到最优决策点。

    既然询问的直线不再满足斜率单调,因此队首的点不能轻易删除,也更不能取队首作为最优决策点了。但是由于数据结构中维护的凸包上相邻的两点之间斜率是单调的,那么我们可以考虑在凸包上二分,找到这样一个位置 \(j\),使得直线可以正好相切于 \(j\) 点,那么这一点就是最优决策点。二分时注意边界的取舍。

    加入点横坐标不单调——平衡树维护凸包/CDQ 分治

    加入点横坐标位于之前的点之间时,可以考虑用平衡树来维护这一凸包。这样就可以支持在 \(O(\log n)\) 的时间里维护凸包和查询。不过相比而言,考虑到代码复杂度,CDQ 分治实现更为常用。因此不再赘述平衡树维护凸包的实现细节。

    如果采用分治实现的话。考虑当前分治区间 \([l,r]\)。需要做的就是计算 \([l,mid]\) 这一部分转移到 \([mid+1,r]\) 的贡献。大致分以下几步:

    1. 先递归处理 \([l,mid]\) 这一区间
    2. \([l,mid]\) 区间内所有点按横坐标排序,用决策单调性的方法维护这一区间内的上(或下)凸包
    3. 之后把 \([mid+1,r]\) 区间内所有的点按询问的直线斜率排序,同样用决策单调性的方法去询问
    4. 递归处理 \([mid+1,r]\) 区间

    这样就可以在 \(O(n\log n)\) 的复杂度内解决这一问题。

    优化:其实归并排序也是分治过程,如果把快排换成归并排序的话,就可以将复杂度优化成 \(O(n\log n)\)

    值得注意的是,处理点横坐标不单调这一情况时也可以同时处理询问直线斜率不单调的情况。

    例题:

    数据结构优化

    数据结构优化 DP 实际上没有太多总结性的东西。主要是需要根据 DP 转移方程,适当地选取数据结构优化转移时的枚举、修改过程。结合数据结构本身对数据维护的的特点对当前的 DP 进行优化。常见的有树状数组、线段树、平衡树、线段树合并、bitset 等等。

    参考资料

    本文作者:AFewMoon,文章地址:https://www.cnblogs.com/AFewMoon/p/15497086.html

    知识共享许可协议

    本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

    限于本人水平,如果文章有表述不当之处,还请不吝赐教。

  • 相关阅读:
    sqoop 使用场景
    scala 类型和集合图
    scala 格式化操作
    sqoop 基础
    scala import 总结
    shell错误 syntax error: invalid arithmetic operator (error token is "
    C/C++ 隐式申明 问题
    阿里云贾扬清:数据湖正成为企业数据应用创新标配
    云原生时代如何用 Prometheus 实现性能压测可观测Metrics 篇
    基于 KubeVela 的机器学习实践
  • 原文地址:https://www.cnblogs.com/AFewMoon/p/15497086.html
Copyright © 2020-2023  润新知