如何在Python中使用斜率优化算法进行查找

2023-04-16 00:00:00 算法 查找 斜率

斜率优化算法(英文名:Convex Hull Trick)可以用来优化一类具有单峰性质的问题,在动态规划问题中,应用较为广泛。例如,在完成一次DP状态转移前,我们需要进行一次斜率优化,则可以将原问题的时间复杂度从$O(n^2)$优化到$O(n \log n)$。

斜率优化算法需要满足如下条件:

  • 函数单调递增或递减
  • 函数的斜率单调递增或递减,即对于两条直线$y=a_1 x+b_1$和$y=a_2 x+b_2$,若$a_1<a_2$,则$b_1<b_2$。

该算法需要用到梯度上升与斜率优化的结合,即找到在$x<j$的位置中与当前位置$i$相关的$j$,并将其斜率与当前位置的斜率做比较,若更倾斜,用其来更新位置$i$处状态的值。

下面是一个例子,可以解决一个类似于最小化最大值的问题:

有一条长为$n$的公路,需要修建$m$个路灯。在公路上安置路灯,每个路灯可以照亮其左侧以及固定长度的右侧。现在需要最小化所有路灯的固定长度之和。

我们可以按照路灯的位置为下标组织动态规划。设$f_i$为前$i$个路灯固定长度之和的最小值,则可得:

$$f_i=\min{f_j+L_j},\text{其中} 0<j<i \text{且第j个路灯至少照亮到i}$$

当函数从左到右单调递增时,我们考虑以$j$作为$x$,$f_j$作为$y$坐标。现在只允许我们在$i$处向上移动,并且$i$处的斜率必须与上一位置的斜率相同,因为如果$f_i$直接大于$f_k+L_k, k < j$,则更新 $f_i$ 而不是 $f_k$。

我们到了位置$i$,它可以表示为一条直线$y=a_ix+b_i$,同时它需要挑选出一个$j$位置,来使得斜率$a_i$最优,即该直线最陡。因为$j<i$,所以$a_j<a_i$是不可能的,即对于任何$i$,$a_i$都是单调递增的。因此只需要考虑在$j<i$中,选择哪个$j$最优。

对于任意$i$,建立一个决策单调性队列,队列中保存的是决策点$j$。队列中的$j$需要满足两个限制条件:

  • $0 < j < i$
  • $i$所确定的直线,和点$j$所确定的直线,在$x=j$处$t$函数值$i$函数值相等。

如果队列中有$j'<j<i$,且点$j'$所确定的直线在点$j$处更优,则退队列。优化过后,队列中的$j$值就变得单调递增了。

代码演示如下:

import sys
from collections import deque

def Convex_Hull_Trick(n, m, L):
    f = [0] * (n+1)
    q = deque()
    q.append(0)

    for i in range(1, n+1):
        while len(q) > 1 and (f[q[0]] - f[q[1]]) <= L[i] * (q[0] - q[1]):
            q.popleft()
        j = q[0]
        f[i] = f[j] + L[i] * (i - j)
        while len(q) > 1 and (f[i]-f[q[-2]])*(q[-1]-q[-2]) <= (f[q[-2]]-f[q[-1]])*(i-q[-2]):
            q.pop()
        q.append(i)
    return f[n]

L = [0, 1, 2, 2, 0]
print(Convex_Hull_Trick(len(L)-1, 3, L))  # --------- 输出:2

在本例中,我们需要在一条长为$n$的公路上安装$m$个路灯,其中每个路灯可照亮其左侧以及固定长度的右侧,需要最小化所有路灯的固定长度之和。因此,我们需要用一个$len(L)$长度为数组的形式表示公路上每个位置需要照亮的长度$L_i$,为了方便计算,增加$L_0=0$,从而规定左侧没有灯照亮。

经过计算,结果为2,即需要设置两个路灯来最小化所有路灯的固定长度之和。

相关文章