参考:
斜率优化dp学习 - orzzz
斜率优化 - OI wiki
HDU3507 Print Article
通过本题目来分析与总结斜率DP。
给定n(5e5)和m(1000),以及一个长为n的数列
A
i
A_i
Ai,现在要把数列分成若干个连续段。
定义每个连续段的代价是
(
Σ
A
i
)
2
+
m
(\Sigma A_i)^2+m
(ΣAi)2+m,求划分后的最小代价。
基础DP
设dp[i]表示把前i个数划分完的最小代价,那么:
d
p
[
i
]
=
m
i
n
{
d
p
[
j
]
+
(
s
u
m
[
i
]
−
s
u
m
[
j
]
]
)
2
+
m
}
,
0
<
=
j
<
i
,
d
p
[
0
]
=
0
dp[i]=min\{dp[j]+(sum[i]-sum[j]])^2+m\},0<=j<i,dp[0]=0
dp[i]=min{dp[j]+(sum[i]−sum[j]])2+m},0<=j<i,dp[0]=0
展开后简写为:
d
[
i
]
=
m
i
n
{
d
j
+
s
j
2
−
2
s
i
s
j
}
+
s
i
2
+
m
d[i]=min\{d_j+s_j^2-2s_is_j\}+s_i^2+m
d[i]=min{dj+sj2−2sisj}+si2+m
斜率变形
设j<k<i,如果dp[i]从k转移优于从j转移,那么有
d
k
+
s
k
2
−
2
s
i
s
k
<
d
j
+
s
j
2
−
2
s
i
s
j
d_k+s_k^2-2s_is_k<d_j+s_j^2-2s_is_j
dk+sk2−2sisk<dj+sj2−2sisj,
变形得到
(
d
k
+
s
k
2
)
−
(
d
j
+
s
j
2
)
<
2
s
i
(
s
k
−
s
j
)
(d_k+s_k^2)-(d_j+s_j^2)<2s_i(s_k-s_j)
(dk+sk2)−(dj+sj2)<2si(sk−sj)
因为
j
<
k
,
s
j
<
s
k
j<k,s_j<s_k
j<k,sj<sk,所以
1
/
2
(
d
k
+
s
k
2
)
−
1
/
2
(
d
j
+
s
j
2
)
s
k
−
s
j
<
s
i
\frac{1/2(d_k+s_k^2)-1/2(d_j+s_j^2)}{s_k-s_j}<s_i
sk−sj1/2(dk+sk2)−1/2(dj+sj2)<si
设
w
i
=
1
/
2
(
d
k
+
s
k
2
)
w_i=1/2(d_k+s_k^2)
wi=1/2(dk+sk2),那么当
w
k
−
w
j
s
k
−
s
j
<
s
i
\frac{w_k-w_j}{s_k-s_j} <s_i
sk−sjwk−wj<si成立时,i从k转移优于从j转移.
如果把所有
(
s
i
,
w
i
)
(s_i,w_i)
(si,wi)视为坐标系上的点,那么连接j和k对应的点,当它们的斜率即
k
j
k
k_{jk}
kjk小于
s
i
s_i
si时,从k转移优于从j转移。
这种转移式变形后通过斜率得到最优转移的方式,就称为斜率DP。
舍弃上凸点,维护下凸包
如果存在a,b,c三个点,满足 s a < s b < s c sa<sb<sc sa<sb<sc且 k b c < k a b k_{bc}<k_{ab} kbc<kab,即b是一个上凸点(如下图),那么b点一定会被舍弃掉。
证明:
s
i
s_i
si一共有三种情况:
-
s
i
<
k
b
c
<
k
a
b
s_i<k_{bc}<k_{ab}
si<kbc<kab,此时a优于b,b优于c
-
k
b
c
<
s
i
<
k
a
b
k_{bc}<s_i<k_{ab}
kbc<si<kab,此时a优于b,c优于b
-
k
b
c
<
k
a
b
<
s
i
k_{bc}<k_{ab}<s_i
kbc<kab<si,此时b优于a,c优于b
综上,从上凸点转移一定不是最优解,可以舍弃。
舍弃掉所有上凸点后,所有可能最优的点会构成一个下凸包,即对于任意三个点a,b,c,当
s
a
<
s
b
<
s
c
s_a<s_b<s_c
sa<sb<sc成立时,总有
k
a
b
<
k
b
c
k_{ab}<k_{bc}
kab<kbc。
每当新加入一个点时,检测其与最右端的两个点是否构成上凸关系,如果构成就删去上凸点,直到只剩下 下凸包 为止。
最优转移
现在只剩下一个问题:对于 s i s_i si,如何从下凸包中选择最优转移?
注意到下凸包中n个点所组成的n-1条线段的斜率是依次递增的,我们可以二分出一个点 j j j,使 k j , j + 1 k_{j,j+1} kj,j+1恰好大于 s i s_i si,此时 j j j就是最优转移。
而当全部的 s i s_i si单调递增时,还可以用队列来维护下凸包,当 k j , j + 1 k_{j,j+1} kj,j+1小于 s i s_i si后,直接将 j j j扔出队列。实际上,为了移除队尾的上凸点,这里的队列应当采用双端队列。
斜率DP算法流程总结
- 找到dp的转移式,通过斜率分析得到点的表示 ( x i , y i ) (x_i,y_i) (xi,yi)及目标斜率的表示。
- 用双端队列维护一个下凸包,每当新来一个点时,末尾不断出队直到构不成上凸包。
- 选择最优解,即 k j , j + 1 k_{j,j+1} kj,j+1大于目标斜率的第一个 j j j。当目标斜率递增时,每次更新队首,然后可以选择队首作为最优解。
代码实现
如果比较时算出斜率,按double比较,会WA。
分别计算
x
i
x_i
xi的差以及
y
i
y_i
yi的差,然后按整数乘起来比较,就过了,浮点数太不可靠了!
/* LittleFall : Hello! */
#include <bits/stdc++.h>
using namespace std; using ll = long long; inline int read();
const int M = 500016, MOD = 1000000007;
ll dp[M], sum[M];
ll q[M], ql, qr;
ll sub_up(int k, int j) //获得w[k]-w[j]
{
return (dp[k]+sum[k]*sum[k]) - (dp[j]+sum[j]*sum[j]);
}
ll sub_do(int k, int j) //获得s[k]-s[j]
{
return 2*(sum[k] - sum[j]);
}
int main(void)
{
#ifdef _LITTLEFALL_
freopen("in.txt","r",stdin);
#endif
int n,m;
while(~scanf("%d%d",&n,&m))
{
for(int i=1; i<=n; ++i)
sum[i] = sum[i-1] + read();
ql = qr = 0; q[qr++] = 0;
for(int i=1; i<=n; ++i)
{
while(qr-ql>=2 &&
sub_up(q[ql+1],q[ql])<=sum[i] * sub_do(q[ql+1],q[ql])
) ++ql; // sum[i]变大,队列pop_front
int j = q[ql];
dp[i] = dp[j] + (sum[i]-sum[j])*(sum[i]-sum[j]) + m; //获得j,计算dp[i]
while(qr-ql>=2 &&
sub_up(q[qr-1],q[qr-2]) * sub_do(i,q[qr-1]) >= sub_do(q[qr-1],q[qr-2]) * sub_up(i,q[qr-1])
) --qr; // 为了将i加入队列,扔掉上凸点,维护下凸包
q[qr++] = i;
}
cout << dp[n] << "\n";
}
return 0;
}
inline int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
其它
如果斜率式是 k j k > s i k_{jk}>s_i kjk>si时 k k k比 j j j优,那么应当维护一个上凸包而不是下凸包。
下凸包( k j k < s i k_{jk}<s_i kjk<si,相邻斜率递增)对应目标斜率 s i s_i si递增,上凸包( k j k > s i k_{jk}>s_i kjk>si,相邻斜率递减)对应目标斜率 s i s_i si递减,满足这两个条件之一时就可以使用双端队列维护凸包。
如果遇上 上凸包+目标斜率递增 或 下凸包+目标斜率递减 呢?这种时候只需要在队尾操作,用单调栈即可。
如果目标斜率没有单调性,就需要在凸包中二分。
如果如果 ( x a − x b ) (x_a-x_b) (xa−xb)的符号无法确定,就需要用平衡树维护凸包,或者整体二分。
这些情况可以在下面的习题博客中了解。
习题
HDU3507 Print Article;即上述例题。
洛谷P3628 [APIO2010]特别行动队;注意尽量不要出现分母为负数的情况。
洛谷P3195 [HNOI2008]玩具装箱TOY;和前面两道基本一样,三道里面做一两道就行。
洛谷P4072 [SDOI2016]征途;二维dp,最好先写出基本dp再去优化。
洛谷P5504 [JSOI2011]柠檬;上凸包对应斜率递增,用单调栈维护凸包。
洛谷P2120 [ZJOI2007]仓库建设;可以作为板子题或练手题使用,检测熟练度。
洛谷P4027 [NOI2007]货币兑换;平衡树维护凸包,凸包中二分斜率,是斜率DP进阶好题