Slope Trick 小记
参考资料:
定义
Slope Trick 是一类具有凸性的最优化 DP 的优化手段,其具有效率高,空间小,易于操作等优点。
对于 Slope Trick 而言,其实现方式较为灵活,取决于题目 凸壳 特点,维护出足以还原整个凸壳/有用部分 的信息,进而根据最优值斜率为零的特点进行操作。
使用 Slope Trick 的 DP 常(并非全部)有如下特点:
- 最优化,有凸性
- 是连续的一次分段函数
- 其斜率变化量是整数,且总变化量不大
一般使用如下信息维护凸壳(并非绝对,具体分析)
-
记录凸壳的起始点\((x,y)\)
-
记录斜率变化点的 \(x\) 坐标(仅需 \(x\) 坐标,是可重集合,一般存储一个表示斜率变化 \(1\)(根据上下凸壳是 \(1/-1\))),如 \(f(x)=|x|\) 存储 \(\lbrace 0,0\rbrace\)
当斜率变化量相当大的时候也可以维护 \((x,\Delta k)\) 表示到 \(x\) 的时斜率变化了 \(\Delta k\)
可并堆由于可以快速合并两个凸壳是极为常用的数据结构,而平衡树由于其堆性质和优秀的扩展性也较为常用。
两个凸函数相加仍然是凸函数。
有一些常见的操作:
-
合并两个凸壳
直接合并起始点,然后合并斜率变化点集合
-
找最值
找到斜率为零的那一段
有一类常见的方法就是开两个堆 \(L,R\) 维护 \(0\) 段左右侧的变化点。
-
加一次函数
直接加入斜率变化点(根据一次函数的边界),同时维护起始位置
-
平移
整体打标记
-
翻转
利用平衡树维护,打标记
统计答案:
- 还原图像找到对应点坐标
- 记录决策点位置还原整个DP过程
平时思考也可以考虑固定点(最左侧最右侧或者最小值点)反推整个图像变化
习题集
这里更多着墨于 Slope Trick 部分,对于 dp 式的得出可能较为简略
CF713C
首先令 \(a_i\leftarrow a_i-i\) 变为单调不降,显然有 dp:\(f_{i,j}=\min_{k\le j}f_{i-1,k}+|j-a_i|\)
由于 \(|j-a_i|\) 为绝对值函数具有凸性,而 $f_{0} $ 是全零,所以 \(f\) 具有凸性。
考虑到 \(\min_{k\le j} f_{i-1,k}\) 得到的凸壳 \(\lbrace(k,g_k)\rbrace\),其实是 \(f_{i-1}\) 所代表的凸壳删去了最小值右侧部分。
所以可以维护最小值位置 \(p\),并舍弃最小值右侧的凸壳(变为一条斜率为 \(0\) 的直线),每次加入 \(a_i\) 时会多出两个转移点 \(\lbrace a_i,a_i\rbrace\) 有如下决策:
- \(p<a_i\) 此时 \(a_i\) 成为新的最小值点(由 \(x\) 最大的转折点往前推斜率),所以第二个 \(a_i\) 应当删去
- \(p\ge a_i\) 此时 \(a_i\) 后面(包括了 \(p\))其斜率会增加 \(1\),这也将导致 \(p\) 点真实斜率增大,应当删去 \(p\),同时整个图像(\(a\) 后面点)整体向上平移了 \(p-a_i\) 的长度(可以看作改变后 \(p\) 这个位置也位于新的零段上面,\(a_i\to p\) 这全部斜率都增加了 \(1\))
CF1534G
首先转曼哈顿,这样就变成了坐标差绝对值的和,然后你每次 \((x-1,y-1),(x-1,y+1)\to (x,y)\),起点 \((0,0)\),这就要求了 \(x,y\) 同奇偶。
显然对于 \((x_i,y_i)\) 你走到了 \(x_i\) 之后再激活他是不劣的,所以我们按照 \(x\) 排序,有如下 \(dp\):
对于后半部分的式子,在凸壳上其实相当于将最小值点左侧向左,右侧向右平移 \(x_i-x_{i-1}\) 个单位长度,然后将最小值变成一个斜率为零的段。
于是可以利用一个大根堆 \(L\) 和一个小根堆 \(R\) 维护斜率为零的段的左侧点和右侧点,同时记录全局标记 \(dl,dr\) 表示当前平移长度。
每次我们讨论 \(y_i\) 的位置。
-
\(y_i\) 位于斜率为零的段上
左侧斜率变化,右侧斜率变化,\(L,R\) 里都插入 \(y_i-dl,y_i+dl\)(注意懒标记是全局的)
-
\(y_i\) 位于斜率为负的段上
这时候 \(L\) 里最大的点移动到正段,也就是 \(R\) 里加入 \(L\) 的最大值,同时后面的图像也向上平移了那么多,增加答案
-
\(y_i\) 位于斜率为正的段上
类似上一情况处理即可。
校内模拟赛(13)-蛋糕
很自然地转化相对关系为往右走或者往上走,则可以得到一个 dp 式 \(f_{x,y}\) 表示走到了 \((x,y)\) 的最小代价,为了简化边界我们钦定 \(a_{n+1}=0\)
有转移:
其中 \(suf\) 是后缀 \(\max\)
注意到也可以写作:
首先可以注意到 \(-ksuf_{x+1}\) 是下凸的,\(\max(a_x-k)\) 是下凸的,初始的 \(f_{0,y}\) 是下凸的(\(f_{0,y}=y·suf_1\)),那么可以假定 \(g_{k}=f_{x-1,k}+\max(a_x-k,0)-ksuf_{x+1}\) 是一个下凸函数,同时加上 \(y·suf_{x+1}\) 这也是个下凸函数,以至于可以归纳有 \(f_x\) 具有凸性。
感性理解其实挺简单的。
那么可以考虑这个操作在干嘛,先分析 \(f,g\) 的关系,相当于是 \(g\) 在最小值后推平成了一条直线,然后再把所有线段斜率加上 \(suf_{x+1}\)
至于 \(g\) 内部在干嘛,相当于是把所有线段斜率减去 \(suf_{x+1}\),然后插入 \(a_x\) 这个斜率转折点,那么不妨看作先插入 \(a_x\),将 \([0,a_x]\) 的斜率减去一,接着将全局斜率减去 \(suf_{x+1}\),删去斜率 \(\ge 0\) 的部分,然后将全局斜率增加 \(suf_{x+1}\)(在这个过程中 \(0\) 点的值肯定是增加了 \(a_i\),因为你相当于是 \(\le a_i\) 的位置都有这个函数的复合)
这样的话,其实也可以当作是原本(插入 \(a\)) 后,将全局斜率 \(>suf_{x+1}\) 的部分删去,替换为 \(suf_{x+1}\) 即可。
动态维护当前的 \(\max k\) 也就是最右侧斜率即可。
同时我们让 \(a_{n+1}=0\),则最终的最右侧凸壳也就是最小值位置,其斜率为零,其坐标(\(y\))显然也是零,因为每个转折点都在其左侧,而我们要求的是 \(f_{n+1,0}\),倒推回去(我们知道最小值位置是 \(0\),用转折点回撤)
APIO2016 烟火表演
容易写出朴素DP式,也就是 \(f_{i,j}\) 表示子树 \(i\) 所有叶子到 \(i\) 距离为 \(j\) 的最小代价,有:
\(f'_{i,j}=\min_{k\le j} f_{i,j}+f_{son,k}+|w(i,son)-(j-k)|\)
发现绝对值函数是凸的,所以叶子的 \(f\) 是凸的,进而这相当于是三个凸包的 \((\min,+)\) 卷积,所以 \(f'\) 是凸的,也即我们归纳证明了 \(f\) 是凸的。由于绝对值函数是下凸的,所以 \(f\) 是下凸的。
然后我们考虑一个绝对值函数有什么影响。
由于我们关心最小值,所以我们只关心斜率为 \(0\) 的段,先看看这一段如何变化。
不难发现假设原本段是 \([l,r]\),变化后等价于 \(\le l\) 的部分向上平移 \(w\),\([l,r]\) 向右平移 \(w\),而新的 \([l,l+w]\) 斜率成了负一,后面的斜率全部改成 \(1\)。
我们只关心最小值,所以我们只需要维护半个凸壳,也就是 \(k\le 0\) 的部分。
那么我们可以忽略 \(k>0\) 的变化,现在问题变成了如何在合并儿子后删掉后面。
其实是简单的,有 \(c\) 个儿子就会有 \(c-1\) 个斜率正的段,直接删掉即可(增加了 \(c-1\) 个拐点)。
(我们并没有管它,但是它合并上来后会形成拐点)
发现我们只需要维护凸壳上点的横坐标,维护即可。
也即使用一个可并堆(大根堆删后面),依次合并儿子,然后弹出前面的 \(c-1\) 个,并再弹出两个作为 \([l,r]\) 整体平移后又插回去,注意到点变化后自然会继承前面的负斜率导致前面一截斜率是 \(-1\)。
这里注意对于叶子节点而言,即使这是一个点也要看成一段,这是 \(c-1\) 个的依据(段段合并)。
CF280E
有教育意义的 Slope Trick。
首先我们可以通过平移将 \([a,b]\) 转移为 \([0,b-a]\),也就是 \(y'_i-y'_{i-1}\in [0.b-a]\),真实 \(y_i=y'_i+(i-1)a\)。可以也先让 \(x_i'=x_i-(i-1)a\)
那么容易有 \(f_{i,j}=\min_{j-k\in [0,b-a]} f_{i-1,k}+(x_i-j)^2\)
这里唯一的问题就是第二维太大了,我们考虑维护函数图像。
考虑到 \((x_i-j)^2\) 也是凸函数,但是现在它不是一次线性函数了,怎么办?
其实我们仍然可以维护二次分段函数,二次函数的平移与求最值也是能够算出的。
所以我们维护这个二次分段函数。
\(\min_{j-k\in [0,b-a]}\) 这个操作相当于将最值所在位置变成一条长为 \(b-a\) 的水平线段,后面的图像往后水平平移 \(b-a\) 个单位长度即可。
这里就相当于原本最值所在的二次函数在最值所在位置分裂成两个,然后插入一条水平线段,可以 \(O(n^2)\) 暴力维护。
每次找到最值位置,就可以求出 \(y'\),再倒推计算贡献就好了。
ABC217H
显然建立 \(B\) 的笛卡尔树,设 \(f[i,j]\) 为树 \(i\) 操作后最大值 \(\le j\) 的最小代价。
显然离开子树后子树都是整体操作的
有
显然可以优化为:
现在我们做到了 \(O(nv)\),显然 \(v\) 可以离散化得到 \(O(n^2)\)
换个写法:
\(siz_i=1\) 时是一条直线
当合并时这个形式感觉由于 \(B\) 递增会有斜率递增的凸性
容易发现由于 \(f\) 的递减,\(siz_i=1\) 的时候有凸性,大胆猜测整个函数有凸性。
首先假定成立,则设 \(s(x)=f_{lc}(x)+f_{rc}(x)\) 有凸性且递减,则比较决策对于 \(j\),\(k ,k+1\),有:
\(s(k)+kB_i-s(k+1)+(k+1)B_i=s(k)-s(k+1)-B_i\)
相当于把 \((k,s(k))\to (k+1,s(k+1))\) 之间的斜率与 \(B\) 取较小值。
那么在原本的斜率递增的情况下会删除一个尾巴/脑袋,还是递增的。
而且至多有 \(siz\) 段不同的斜率,每段最多删一次。
考虑用数据结构维护斜率,然后每次删除增加,我们需要启发式合并?。
不妨维护 \((pos,\Delta y)\)(\(\Delta x=1\)),用可并堆(\(pos_{\min}\) 的左偏树就行),这里是由于斜率变换量较大,维护每个位置的斜率变化总量
讲讲实现(谁再喷我水):
维护 \(f0_i\) 表示树 \(i\) 凸包的首项,也就是上面的 \(f_{i,0}\)。
然后维护 \((pos,\Delta y)\),比如一棵树初始化是 \((0,b_i),(a_i,0)\),和其两个子树的凸包合起来。插入 \((0,b_i)\) 这是因为实现方程里面的 \(f[i,j]=\min_{k=0}^{\max(A_i-j,0)}(f[lc,j+k]+f[rc,j+k]+(j+k)B_i)-jB_i\)。\(j+k\) 这一项,而 \((a_i,0)\) 是卡住上界。
(可以看作是三个凸包,\(f[lc],f[rc]\) 以及 \(0,B_i,2B_i……\) 的和)
然后我们将斜率小于零的部分以及初始位置小于 \(a_i\) 的部分全部删去(因为你必须要操作根节点)。注意删完了要加一个新的首项进去。
接着我们插入 \((0,-b_i)\) ,这是因为实现方程里面的 \(f[i,j]=\min_{k=0}^{\max(A_i-j,0)}(f[lc,j+k]+f[rc,j+k]+(j+k)B_i)-jB_i\)。
注意到我们只需要维护这些二元组的快速合并和查首项,用可并堆(左偏树)即可。