下标序列是我定义的一个东西。对于dp方程
f
[
i
]
=
min
(
f
[
j
]
+
c
o
s
t
(
i
,
j
)
)
,
l
[
i
]
≤
j
≤
r
[
i
]
f[i] = \min(f[j] + cost(i,j)),l[i]\le j\le r[i]
f[i]=min(f[j]+cost(i,j)),l[i]≤j≤r[i] 我们维护一个序列保存所有可以从那里转移来的位置。比如枚举到
i
i
i时,序列里存的就是
l
[
i
]
l[i]
l[i]到
r
[
i
]
r[i]
r[i]的所有数。那么我们最终的决策点
k
k
k一定是从这个序列里面选的。这个就叫下标序列。
因为要枚举
n
n
n次,每次下标序列中有
n
n
n个元素,所以复杂度是
O
(
n
2
)
O(n^2)
O(n2)。
但是我们发现,对于相邻的
i
i
i,下标序列可能相差不大。于是我们考虑从这里优化。
考虑对全局维护一个下标序列F,每次枚举
i
i
i时在这个序列上面作修改,使得它成为每个i自己的下标序列。
如果每次我们可以快速地从当前下标序列中找到决策点,就可以大大提高效率。事实上,斜率优化就做了这件事情。斜率优化支持用
O
(
1
)
或
O
(
log
n
)
O(1)或O(\log n)
O(1)或O(logn)的时间找到决策点和维护下标序列。具体地说,找决策点可以直接取队首元素或二分查找,维护序列可以用数组或平衡树。
接下来就比较跳跃地介绍斜率优化方法。考虑把下标序列中的每一个元素当做一个平面上的点,它的横纵坐标都可以由这个元素的值(就是这个下标)计算得到。我们用一个折线把这些点从左到右连起来。
然后对于每次询问,假设这个询问给了一个可以随意平移的直线,也就是给定一个斜率。我们要找的决策点,就是这个直线与折线在最下面的切点。
如果这个折线是一个向下凸的“弧”,那就可以二分答案,看斜率在哪里刚好比答案大,那就是答案。【1】
这个就很好啊,那怎么把它和dp方程联系起来呢?
刚才我们说了维护的是下标序列,并且有决策单调性。那么我们只要发现一个点以后不再可能是答案,那就可以把它踢出下标序列。刚才我们已经发现了!如果折线是向下凸的弧,那答案一定在这个弧上。如果折线不是向下凸的,那我们就找到向下凸的弧,然后不是弧上的就都踢掉就行了。
事实上,我们不是每次找一个弧然后删除一些点,而是维护这个弧,并且每次加入一个新点时,把它加入弧,然后把弧以上的点都删掉。其实这就是
O
(
1
)
O(1)
O(1)维护凸包。如果新点总是在最右边加入,就用一个数组(栈)维护,每次先把后面的弹了再加入;否则就用平衡树
O
(
log
n
)
O(\log n)
O(logn)维护。
回到刚才【1】的地方,我们知道二分答案比较慢,于是也可以用踢掉不需要的东西的办法。如果询问给的直线每次的斜率都比上一次大,那么所有斜率比它小的点就可以直接删除。这其实是一个单调队列,复杂度降为
O
(
1
)
O(1)
O(1)。
最后两个问题:怎么用下标序列计算点的坐标?每次询问给的直线是哪里来的?这个其实都是用dp方程变形得到的。我们把dp方程化成这样的方程
f
[
i
]
=
g
(
i
)
h
(
j
)
+
q
(
i
)
+
s
(
j
)
f[i]=g(i)h(j)+q(i)+s(j)
f[i]=g(i)h(j)+q(i)+s(j)
变成
s
(
j
)
=
−
g
(
i
)
h
(
j
)
−
q
(
i
)
+
f
[
i
]
s(j)=-g(i)h(j)-q(i)+f[i]
s(j)=−g(i)h(j)−q(i)+f[i]
我们看到与
j
j
j有关的项,把
h
(
j
)
h(j)
h(j)的看成横坐标,
s
(
j
)
s(j)
s(j)看成纵坐标,那么:
y
j
=
−
g
(
i
)
x
j
−
q
(
i
)
+
f
[
i
]
y_j=-g(i)x_j-q(i)+f[i]
yj=−g(i)xj−q(i)+f[i]
看,
(
x
j
,
y
j
)
(x_j,y_j)
(xj,yj)就是下标序列每个对应的点,把它代入直线
y
=
−
g
(
i
)
x
−
q
(
i
)
+
f
[
i
]
y=-g(i)x-q(i)+f[i]
y=−g(i)x−q(i)+f[i],其中i是枚举的可以当成常数,要求的是
f
[
i
]
f[i]
f[i],使它最小。
懂了没?询问的斜率就是
−
g
(
i
)
-g(i)
−g(i),每个点就是
(
h
(
j
)
,
s
(
j
)
)
(h(j),s(j))
(h(j),s(j))。为什么要找切点?这样纵截距
f
[
i
]
f[i]
f[i]就最小啊!