用图像斜率可以将一些问题由O(n2)降到O(n)的算法。
下面结合一道经典题目介绍斜率优化
(洛谷链接)[HNOI2008]玩具装箱
这是一道典型的斜率优化题目。题目不再赘述,直接说与斜率优化相关的部分。
设
f
[
i
]
f[i]
f[i]为填装前
i
i
i个玩具需要的最小花费,
s
[
i
]
s[i]
s[i]为前
i
i
i个物品的总长度。那么可以得到:
f
[
i
]
=
m
i
n
j
=
0
i
−
1
{
f
[
j
]
+
(
s
[
i
]
−
s
[
j
]
+
i
−
j
−
1
−
L
)
2
}
f[i]=min_{j=0}^{i-1}\{ f[j]+(s[i]-s[j]+i-j-1-L)^2 \}
f[i]=minj=0i−1{f[j]+(s[i]−s[j]+i−j−1−L)2}
如果直接算的话复杂度为O(n2),会超时的,所以需要优化。
我们将这个式子进行整理和移项:
f
[
i
]
=
f
[
j
]
+
[
(
s
[
i
]
+
i
)
−
(
s
[
j
]
+
j
+
1
+
L
)
]
2
f[i]=f[j]+[(s[i]+i)-(s[j]+j+1+L)]^2
f[i]=f[j]+[(s[i]+i)−(s[j]+j+1+L)]2
f
[
i
]
=
f
[
j
]
+
(
s
[
j
]
+
j
+
1
+
L
)
2
+
(
s
[
i
]
+
i
)
2
−
2
(
s
[
i
]
+
i
)
(
s
[
j
]
+
j
+
1
+
L
)
f[i]=f[j]+(s[j]+j+1+L)^2+(s[i]+i)^2-2(s[i]+i)(s[j]+j+1+L)
f[i]=f[j]+(s[j]+j+1+L)2+(s[i]+i)2−2(s[i]+i)(s[j]+j+1+L)
2
(
s
[
i
]
+
i
)
(
s
[
j
]
+
j
+
1
+
L
)
+
f
[
i
]
−
(
s
[
i
]
+
i
)
2
=
f
[
j
]
+
(
s
[
j
]
+
j
+
1
+
L
)
2
2(s[i]+i)(s[j]+j+1+L)+f[i]-(s[i]+i)^2=f[j]+(s[j]+j+1+L)^2
2(s[i]+i)(s[j]+j+1+L)+f[i]−(s[i]+i)2=f[j]+(s[j]+j+1+L)2
为了方便观察,我们令
a
=
s
[
i
]
+
i
,
b
=
s
[
j
]
+
j
+
1
+
L
a=s[i]+i,b=s[j]+j+1+L
a=s[i]+i,b=s[j]+j+1+L
现在这个式子变为
2
a
b
+
f
[
i
]
−
a
2
=
f
[
j
]
+
b
2
2ab+f[i]-a^2=f[j]+b^2
2ab+f[i]−a2=f[j]+b2
其中
a
a
a只与
i
i
i有关,
b
b
b只与
j
j
j有关。
由于我们求的是
f
[
i
]
f[i]
f[i],而
j
j
j的相关信息是之前已经知道的,若把
f
[
j
]
+
b
2
f[j]+b^2
f[j]+b2看做
y
y
y,把
b
b
b看做
x
x
x,我们便得到下面这个直线方程:
y
=
2
a
x
+
f
[
i
]
−
a
2
y=2ax+f[i]-a^2
y=2ax+f[i]−a2
这样,
j
j
j的信息就被表示成了点。也就是说,我们要从当前
i
i
i个左右的点中找到一个点,使得
f
[
i
]
f[i]
f[i]最小,这个点就是
j
j
j点。
(图:绿线是边界,黄线是目标直线,其斜率为
2
a
2a
2a)
这个方程的斜率是 2 a ( 2 a > 0 ) 2a(2a>0) 2a(2a>0), f [ i ] f[i] f[i]最小时也就是这条直线在 y y y轴上的截距最小时,(和高中学过的线性规划类似)我们把一条斜率 2 a 2a 2a的直线从最下面开始慢慢向上平移,这条直线第一个碰到的点就是 j j j点。
显然,可能是
j
j
j点的点都在边界上。又因为
2
a
2a
2a是随着i增长的,于是我们可以用单调队列求
f
[
i
]
f[i]
f[i]:
(用slope(
i
i
i,
j
j
j)表示两个点的斜率)
1.对head: while(slope(head,head+1)<2a) head++;
2.求出
f
[
i
]
f[i]
f[i]:f[i]=y+a^2-2ax
3.对tail: while(slope(tail-1,tail)>slope(tail-1,i)) tail--;
4.
i
i
i点入队
对于第1步,理由是
2
a
2a
2a是递增的,此时斜率小于
2
a
2a
2a的两点的前一点一定不会是
j
j
j。
对于第3步,理由是若slope(tail-1,tail)>slope(tail-1,
i
i
i),那么tail点会被tail-1和
i
i
i点包围,这样tail就不再可能是
j
j
j。
编程具体细节还会有需要注意的地方,但由于跟算法无关,在此就不说明了。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
#define a(i) (s[i]+i)
#define b(i) (s[i]+i+L+1)
#define x(i) b(i)
#define y(i) (f[i]+b(i)*b(i))
#define slope(i,j) (y(j)-y(i))*1.0/(x(j)-x(i))
const int N=5e4+10;
int n, L;
LL s[N], f[N];
int q[N], head, tail;
int main()
{
scanf("%d%d", &n, &L);
for(int i=1; i<=n; ++i)
{
scanf("%lld", s+i);
s[i]+=s[i-1];
}
//
head=tail=0;
q[tail++]=0;
for(int i=1; i<=n; ++i)
{
while(head+1<tail&&slope(q[head],q[head+1])<2*a(i)) head++;
f[i]=y(q[head])+a(i)*a(i)-2*a(i)*b(q[head]);
while(head+1<tail && slope(q[tail-2],q[tail-1])>slope(q[tail-2],i)) tail--;
q[tail++]=i;
}
//
printf("%lld\n", f[n]);
return 0;
}
改进与注意
1. 对 slope(i,j)>slope(u,v)的改进:
(
y
i
−
y
j
)
(
x
u
−
x
v
)
>
(
y
u
−
y
v
)
(
x
i
−
x
j
)
(y_i-y_j)(x_u-x_v)>(y_u-y_v)(x_i-x_j)
(yi−yj)(xu−xv)>(yu−yv)(xi−xj)
2. tail-- 部分及 head++ 部分可用二分查找优化。
3. 注意 1. 中的 (xu-xv)>0 和 (xi-xj)>0