Slope trick优化dp
适用于一类dp值关于下标的函数是连续函数,分段函数,凸函数,每一段需要是一次函数,需要是整数斜率。常见于一些最小调整代价题,因为经常会有\(|x-y|\)这种典型符合上述要求的函数出现,而且这类dp通常会有对应下标相加的形式出现。
我们考虑通过最右一段的一次函数\(y=kx+b\),和前面的分界点来表示这个函数。要求相邻段之间函数的斜率差为\(1\),也就是说如果真正的函数里有相邻两段之间斜率差不为\(1\)的话,这个分界点要在数据结构里出现多次。比如下面这个函数:
那我们需要维护一个最右段函数\(k=2,b=-1\),和一个分界点集合\(S=\{-1,1,1\}\)。
两个满足条件的函数相加的话,就是最右段函数相加,分界点集合取并。因为都是连续函数,只要维护的斜率是对的,函数值就肯定就是对的。
但是你可以发现维护最右段函数并不重要,分界点集合才重要。所以说最右段函数只是其中一种维护方式。通过几道题来用一下。
CF713C
首先严格递增这个事很烦,把他转化成单调不降,就是令\(a_i-=i\)。然后暴力dp就很显然了。令值域为\(m\)。
\(dp(i,j)=\min\limits_{k=1}^jdp(i-1,k) +|a_i-j|\)
用函数来代替这个\(dp\)的第二维\(f_i(x)=dp(i,x)\)。设函数\(g_i(x)\)为\(dp(i,j)\)第二维的前缀最小值对应的函数。那么有
\(f_i(x)=g_i(x)+|x-a_i|\)
考虑用维护最右段函数的方式维护\(f\)和\(g\)。一个下凸函数取前缀最小值的话,相当于是把斜率为\(0\)后面的段全部去掉,变成和这段一样。这个\(h(x)=|x-a_i|\)是一个很简单的函数,他的最右段函数就是\(x-a_i\),分界点集合就是\(\{a_i,a_i\}\)。用一个优先队列维护即可,复杂度\(O(n\log n)\)。
回顾一下刚才的过程,实际上slope trick优化dp大多数就是转移时构造一个\(g_i(x)\)替代那一坨\(min\),把转移方程转化为对应下标相加的形式。用维护最右段函数来表示这个函数的优势实际上在于这个构造出来的函数如果是前缀最小值的话,非常容易维护。同样的,如果是后缀最小值,可以考虑维护最左段函数。
CF1534G
推性质部分见我的置顶博客去Ctrl+F。快进到优化dp部分。还有,这个dp需要整数定义域是一段连续区间,否则不能维护。那么这个题可以通过倒着设状态来规避dp值里有正无穷的问题。
这里的\(\Delta i\),\(i'\),\(x'\)对于固定的\(i\)都是固定的,不用管。第二个\(\sum\)号暴力枚举复杂度是对的。
设\(f_i(x)=dp(i,j),g_i(x)=\min\limits_{t=x}^{x+\Delta i}f_i(t)\)
那么有
这个\(g_i(x)\)形式不太好看,由于他不是前缀min也不是后缀min,影响到的东西比较多。但是他是一段包含自己的区间,对于一个下凸函数,\(x\)下标函数值变成一段包含自己区间的函数值最小值是很有规律的。我们可以考虑维护中间斜率为\(0\)的一段的截距\(y=b\)。设中间那段区间为\([L,R]\)(分段连续函数开区间闭区间不重要)。
那么对于\(x\geq L\)的部分,函数值显然没有变化。对于左边,相当于把这段区间拉长为\([L-\Delta i,R]\),\(L\)左边的部分整体平移到\(L-\Delta i\)左边。所以用一个大根堆,一个小根堆分别维护两侧的分界点集合,对于一次从\(f_i(x)\)变成\(g_i(x)\),把左侧的堆整体打一个标记即可。
往里加一个\(h(x)=|x-v|\)的话,就要对于\(v\)和中间段端点\([L,R]\)大小关系分类讨论了。在区间内的话,直接往左插一个\(v\),往右插一个\(v\)。在左边的话,就插入两个\(v\),然后把左边堆顶弹出来插入右边堆即可。过程有点类似对顶堆。右边类似。
这么做是利用凸函数区间min的性质。只要维护出来中间段特殊的地方即可。
APIO2016烟花表演
dp是非常显然的。对于\(u\)的每个儿子\(v\),设\(w\)为\((u,v)\)边权,有\(dp(u,i)+=\min\limits_{t=0}^i\{dp(v,t)+|w-(i-t)|\}\)
这个形式其实就很复杂了。还是设\(f_u(x)=dp(u,x),g_v(x)=\min\limits_{t=0}^i\{f_v(t)+|x-(w+t)|\}\)。
虽然形式很复杂,而且没法把\(x\)和枚举min的\(t\)拆开。那就整体考虑,但是还是和上一个题一样,这是个凸函数,只要是一段区间的最小值肯定有很好的性质。这是个前缀的最小值,虽然不是\(f\)的前缀最小值。。所以说就肯定和中间最低的一段\([L,R]\)有关。具体就不展开了,借助都是整数斜率这个性质,自己分类讨论手推一下容易得到:
别管看起来多花里胡哨,别忘了我们只需要维护分界点集合和某一段函数。观察这个形式发现斜率大于\(0\)的只有一段,那就可以考虑维护最右段函数。只要能把最右段函数维护出来,前面式子根本不用管,就把分界点弄对就行。仔细观察一下发现\(f_v(R)\)需要把所有斜率大于\(0\)的分界点都弹掉。根据合并的性质,一个函数斜率大于\(0\)的段只有儿子个数个,所以暴力弹就对。分界点集合的话,发现就是删掉一个\(L\)和\(R\),加入一个\(L+w\)和\(R+w\)。然后两个儿子合并的话可以启发式合并就\(O(n\log^2 n)\)。写可并堆就少一个\(\log\)。