肯定还有一些地方讲的不太对,留个坑以后再填
概述
全 文 转 移 方 程 中 的 状 态 都 为 f 。 全 文 两 点 间 的 斜 率 统 一 表 示 为 g [ x , y ] {\color{Red}全文转移方程中的状态都为 f。} \\{\color{Green}全文两点间的斜率统一表示为g[x,y]} 全文转移方程中的状态都为f。全文两点间的斜率统一表示为g[x,y]
众所周知, 计算斜率的式子大概长这样:
x
i
−
x
j
y
i
−
y
j
\frac{x_i-x_j}{y_i-y_j}
yi−yjxi−xj
用的上斜率优化的题目大概长啥样呢?它们的方程可以近似地看成这样:
f i = max j = 1 i − 1 { f j + c i , j } f_i = \max_{j=1}^{i-1} \{f_j+c_{i,j}\} fi=j=1maxi−1{fj+ci,j}
感性理解一下, 大概就是说每一个状态,都可以从之前的状态花费一定的代价转移过来。
一般这样的方程,每次计算,我们都需要遍历一遍之前的状态,这样会造成极大的时间浪费,达到 O ( n 2 ) O(n^2) O(n2) 甚至 O ( n 3 ) O(n^3) O(n3) 级别的时间复杂度。
如果你已经学习过了单调队列优化DP,那么你可能会想:
如果我计算 f i f_i fi 时,已经找到了之前状态的最佳转移点,那该多好?
其实斜率优化DP,就是通过一些技巧,令状态之间有比较优劣的依据,以此来用单调队列维护它们的单调性,从而使得每一次计算新的状态的时候,都可以直接从单调队列的队头取出最优的转移出发点来。
比如说状态 f i f_i fi 可以从 f j f_j fj 和 f k f_k fk 转移而来,且满足 j < k < i j < k <i j<k<i 。
那么对于状态 f j f_j fj 和 f k f_k fk 的优劣性,分如下几种情况讨论:
- 从 f j f_j fj 转移到 f i f_i fi 优于从 f k f_k fk 转移到 f i f_i fi, 但是很可惜,因为 k k k 的位置比 j j j 靠后,所以暂时先不能把 k k k 删掉。
- 从 f k f_k fk 转移到 f i f_i fi 优于从 f j f_j fj 转移到 f i f_i fi, j j j 就可以 o u t out out 了。
这些其实就是单调队列的基本思想了。
接下来考虑一下怎么把斜率这鬼玩意给牵扯进来:
假设队列中现在有五个点,分别代表DP中的五个状态: a , b , c , d , e a,b,c,d,e a,b,c,d,e
首先保证,五个点中,任意两个点的转移式都可以表达成上文的斜率 x i − x j y i − y j \frac{x_i-x_j}{y_i-y_j} yi−yjxi−xj 形式
因此,如果我们在图上将四对相邻的点的转移式画成一条斜率对应的直线——
我们会发现,此时,
g
[
a
,
b
]
>
g
[
b
,
c
]
<
g
[
c
,
d
]
>
g
[
d
,
e
]
g[a,b]>g[b,c]<g[c,d]>g[d,e]
g[a,b]>g[b,c]<g[c,d]>g[d,e]
而用单调队列维护好的状态集合大概长这样:
此时,
g
[
a
,
b
]
>
g
[
b
,
c
]
>
g
[
c
,
d
]
>
g
[
d
,
e
]
g[a,b]>g[b,c]>g[c,d]>g[d,e]
g[a,b]>g[b,c]>g[c,d]>g[d,e]
没错,我知道你会问,怎么保证用单调队列维护好斜率后,队首就是最佳的转移出发点呢?
下面是一两道简单的例题,用它们来讲讲吧。
例题1-特别行动队(BZOJ1911)
这一题算是比较简单的斜率优化了,式子也比较好推,没有那么反人类
本着人道主义精神,我还是决定把题面截图放在下面:
接下来,我们通过这道题来了解一下,可以用斜率优化解决的题目大概长啥样。
设:
- f i f_i fi 表示将前 i i i 名士兵划分成完整的若干组修正后最大战力值。
- s i s_i si 表示前 i i i 名士兵的 x i x_i xi 之和。
一个很显然的方程就出来了,其实就是加上了
[
j
+
1
,
i
]
[ j+1,i ]
[j+1,i] 中的所有士兵划分为一队的花费,按照题意写下来就行。
f
i
=
max
{
f
j
+
a
(
s
i
−
s
j
)
2
+
b
(
s
i
−
s
j
)
+
c
}
f_i = \max\{f_j + a (s_i-s_j)^2+b (s_i - s_j) + c \}
fi=max{fj+a(si−sj)2+b(si−sj)+c}
接下来比较从
f
j
f_j
fj 转移到
f
i
f_i
fi 和 从
f
k
f_k
fk 转移到
f
i
f_i
fi 的优劣性。
设 k < j < i k<j<i k<j<i 。
如果 选
j
j
j 优于选
k
k
k,那么显然满足下式:
f
j
+
a
(
s
i
−
s
j
)
2
+
b
(
s
i
−
s
j
)
+
c
>
f
k
+
a
(
s
i
−
s
k
)
2
+
b
(
s
i
−
s
k
)
+
c
f_j + a (s_i-s_j)^2+b (s_i - s_j) + c > f_k + a (s_i - s_k)^2+b (s_i-s_k)+c
fj+a(si−sj)2+b(si−sj)+c>fk+a(si−sk)2+b(si−sk)+c
首先显然可以将
c
c
c 项消去。接着运用完全平方公式拆开后,将
a
s
i
2
a {s_i}^2
asi2 和
b
s
i
b {s_i}
bsi 项消去。移项后可得:
f
k
−
f
j
+
a
s
k
2
−
a
s
j
2
−
b
s
k
+
b
s
j
>
2
a
s
i
s
j
−
2
a
s
i
s
k
f_k-f_j+a{s_k}^2-a{s_j}^2-bs_k+bs_j > 2a{s_i}{s_j}-2a{s_i}{s_k}
fk−fj+ask2−asj2−bsk+bsj>2asisj−2asisk
不等式右边的
s
j
−
s
k
s_j-s_k
sj−sk 移到左边作为分母:
f
k
−
f
j
+
a
s
k
2
−
a
s
j
2
−
b
s
k
+
b
s
j
s
j
−
s
k
>
2
a
s
i
\frac{f_k-f_j+a{s_k}^2-a{s_j}^2-bs_k+bs_j}{s_j-s_k} > 2a{s_i}
sj−skfk−fj+ask2−asj2−bsk+bsj>2asi
将不等式左边整理一下:
( f k + a s k 2 − b s k ) − ( f j + a s j 2 − b s j ) s j − s k \frac{(f_k+a{s_k}^2-bs_k)-(f_j+a{s_j}^2-bs_j)}{s_j-s_k} sj−sk(fk+ask2−bsk)−(fj+asj2−bsj)
注意到了吗?这其实就是一个斜率式!!!
因此,这道题可以用斜率优化DP式。
计算斜率的代码至此就很显然了,下面的代码只算出了不等式的左边部分:
inline double work(ll k, ll j) {
return (double) (f[k] - f[j] + a*s[k]*s[k] - a*s[j]*s[j] - b*s[k] + b*s[j]) / (s[k] - s[j]);
}
单调队列部分就更简单了:
- 处理队首时,将计算出来的斜率和 2 a s i 2as_i 2asi 作比较即可。
- 处理队尾时,注意维护此时状态集合的“下凸”性质即可(就是上面的第二张图)
for (int i = 1; i <= n; ++ i) {
while (head + 1 <= tail && work(q[head], q[head + 1]) > 2 * a * s[i])
++ head;
ll j = q[head];
f[i] = f[j] + a * (s[i] - s[j]) * (s[i] - s[j]) + b * (s[i] - s[j]) + c;
while (head + 1 <= tail && work(q[tail - 1], q[tail]) < work(q[tail], i))
-- tail;
q[++ tail] = i;
}
这里还有一个小小的细节需要注意:不能像往常那样写
head<=tail
判断队列是否为空,而要保证队列中至少还剩下 2 2 2 个元素
以下是完整代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
ll n, a, b, c, tmp, f[N], q[N], s[N];
inline double work(ll k, ll j) {
return (double) (f[k] - f[j] + a * s[k] * s[k] - a * s[j] * s[j] - b * s[k] + b * s[j]) / (s[k] - s[j]);
}
int main() {
scanf("%lld%lld%lld%lld", &n, &a, &b, &c);
for (int i = 1; i <= n; ++ i) {
scanf("%lld", &tmp);
s[i] = s[i - 1] + tmp;
}
ll head = 1, tail = 1;
for (int i = 1; i <= n; ++ i) {
while (head + 1 <= tail && work(q[head], q[head + 1]) > 2 * a * s[i])
++ head;
ll j = q[head];
f[i] = f[j] + a * (s[i] - s[j]) * (s[i] - s[j]) + b * (s[i] - s[j]) + c;
while (head + 1 <= tail && work(q[tail - 1], q[tail]) < work(q[tail], i))
-- tail;
q[++ tail] = i;
}
printf("%lld", f[n]);
return 0;
}
小结
因此,斜率优化的DP式子需要能够化成斜率式的形式,即为:
x
i
−
x
j
y
i
−
y
j
\frac{x_i-x_j}{y_i-y_j}
yi−yjxi−xj
其中, x i , y i x_i,y_i xi,yi项与下标值包含 a a a 的项有关,也就是不能有下标包含 b b b ; x j , y j x_j,y_j xj,yj 反之亦然。
拿上面那道题的不等式左边来理解一下:
(
f
k
+
a
s
k
2
−
b
s
k
)
−
(
f
j
+
a
s
j
2
−
b
s
j
)
s
j
−
s
k
\frac{(f_k+a{s_k}^2-bs_k)-(f_j+a{s_j}^2-bs_j)}{s_j-s_k}
sj−sk(fk+ask2−bsk)−(fj+asj2−bsj)
可以将其变成:
(
−
f
j
−
a
s
j
2
+
b
s
j
)
−
(
−
f
k
−
a
s
k
2
+
b
s
k
)
s
j
−
s
k
\frac{(-f_j-a{s_j}^2+bs_j)-(-f_k-a{s_k}^2+bs_k)}{s_j-s_k}
sj−sk(−fj−asj2+bsj)−(−fk−ask2+bsk)
可以发现,这样就满足上述性质了。
因此,我们对于每一道斜率优化DP题目的目标就是将DP式子转化成:
x i , y i x_i,y_i xi,yi项与下标值包含 a a a 的项有关,也就是不能有下标包含 b b b ; x j , y j x_j,y_j xj,yj 反之亦然。
再讲两道例题,加深一下印象吧。