更新历史
前言
前置芝士
线性 dp
斜率优化模板
思路
若有一维的 dp 方程形如
F(i)=j∈[0,i)min{f(j)−ai×dj}
其中 f(j) 必须包含 dpj 且只与 j 有关,F(i) 必须包含 dpi 且只与 i 有关
且满足
aj≤ai,dj≤di(j<i)
则暴力算需要 O(n2) 的时间复杂度。而且不能直接用单调队列优化,因为它有一个既包含 i 又包含 j 的项 ai×dj。
遇到这种方程,就可以用斜率优化来求解。
考虑如何求出 F(i) 的值。
我们将原方程作一些换元
b=F(i),yj=f(j),k=ai,xj=dj
则原方程变为了
b=j∈[0,i)min{yj−k×xj}
其中 k 和 x 是单调递增的。
再令 bj=yj−k×xj,则 F(i)=b=minj∈[0,i)bj。
观察 bj 的表达式,可以发现 bj 刚好是「斜率为 k 且过 (xj,yj) 的直线」的截距。
也就是说,如果我们能用 O(1) 的时间算出最小的截距对应的 j,就能做到 O(1) 转移,总体复杂度也就是 O(n) 了。
那么我们把所有 j 对应的点 Pj(xj,yj) 描出来,如下图就是一种 i=5 时的情况:
注意,由于 x 的单调递增,每个点是从左往右排的。
容易发现,将一条斜率为 k 的直线从下往上平移,直至碰到其中一个点,这时这条直线的截距 b 最小,碰到的点的编号就是最佳转移的 j 值。比如上图中的最佳转移的 j 值就为 3。
而显然,第一个碰到的点一定在这 i−1 个点构成的下凸壳上,所以我们不需要枚举前面所有的 (x,y),而只需要枚举在下凸壳上的 (x,y) 即可。
但是面对极端数据,这样的做法还是会达到 O(n2) 的时间复杂度,怎么办呢?
我们发现上面还有一个斜率 k 单调递增的条件没有用上。
加上这个条件,我们发现这个碰到的点除了会在下凸壳上,而且它的编号还是单调不减的 (感性理解一下) 。
那就好办了。每次找到下凸壳上会碰到的点后,将这个点之前的点全部从下凸壳删除,就可以保持总体 O(n) 的时间复杂度(每个点最多被查询一次,就会被删除)。
接下来就是一些实现的写法,由于用到了斜率来维护所以叫斜率优化。
下面的说明中,K(p1,p2) 代表第 p1 和 p2 个点所连成的直线的斜率。
X(p) 指上文提到的第 p 个点所对的 x=di,Y(p) 同理。
一:如何查找下凸壳上第一个碰到的点,并删除之前的点
假设现在已经有的下凸壳是由,下标为 ql 至 qr 的点构成的(实际上就是一个单调队列,维护的是相邻2个点间的斜率递增),那么算出下标 i 对应的 ki。
在 q 中还有至少2个点时,比较 K(ql,ql+1) 与 ki 的大小关系。若前者较小,则弹出队首 l++
。否则对应的 l 就是我们要找的那个点。
代码如下:
1 2 3 4 5 6 7
| double K(int p1,int p2){ int x=X(p1),y=Y(p1),x2=X(p2),y2=Y(p2); return (y2-y)*1.0/(x2-x); } while(l<r&&K(q[l],q[l+1])<k[i]) l++;
int j=q[l];
|
当然,开始时要存入一个编号为 0 的点,也就是固定值 dp0。
为了避免精度问题,也可以用乘法来比较斜率:
1 2 3 4 5 6 7
| bool cmp1(int p1,int p2,int kk){ int x=X(p1),y=Y(p1),x2=X(p2),y2=Y(p2); return (y2-y)<kk*(x2-x); } while(l<r&&cmp1(q[l],q[l+1],k[i])) l++;
int j=q[l];
|
二:如何维护下凸壳
我们在通过上面算出最佳的 j 后,就可以对 dpi 进行转移。转移完以后,就可以算出对应的点 Pi(x,y)。
在 q 中还有至少2个点时,比较 K(qr−1,qr) 和 K(qr,i) 的大小。若前者较大,则弹出队尾 r--
。否则退出循环,将第 i 个点加入下凸壳 q[++r]=i
。
代码如下:
1 2 3 4 5 6 7
| double K(int p1,int p2){ int x=X(p1),y=Y(p1),x2=X(p2),y2=Y(p2); return (y2-y)*1.0/(x2-x); } while(l<r&&K(q[r-1],q[r])>K(q[r],i)) r--;
q[++r]=i;
|
改成乘法如下:
1 2 3 4 5 6 7
| bool cmp2(int p1,int p2,int p3){ int x=X(p1),y=Y(p1),x2=X(p2),y2=Y(p2),x3=X(p3),y3=Y(p3); return (y2-y)*(x3-x2)>(y3-y2)*(x2-x); } while(l<r&&cmp2(q[r-1],q[r],i)) r--;
q[++r]=i;
|
模板例题
例题:P3628 特别行动队
洛谷传送门
给定一个长度为 n 的序列 x,以及一个二次函数 F(X)=A⋅X2+B⋅X+C。要求将序列分成若干段连续区间,一段区间 [l,r] 的权值为 F(i=l∑rxi),求最大权值和。
解题思路
这题是一道斜率优化模板题。
首先列出原始的 dp 方程:
dpi=j∈[0,i)max{dpj+A×(sumi−sumj)2+B×(sumi−sumj)+C}
其中 sum 表示 x 的前缀和。
化简之后得到:
dpi=j∈[0,i)max{dpj+A×sumi2−2A×sumisumj+A×sumj2+B×sumi−B×sumj+C}
dpi−A×sumi2−B×sumi−C=j∈[0,i)max{dpj+A×sumj2−B×sumj−2A×sumisumj}
这时候,y,k,x,b 的取值就能确定了:
y=f(j)=dpj+A×sumj2−B×sumj
k=ai=2A×sumi
x=dj=sumj
b=dpi−A×sumi2−B×sumi−C
b 的取值虽然也确定了,但是程序中用不到,写出来就是为了好理解一些。
然后就用上面的板子套就行了。
唯一要注意的是,此题求的是最大值,所以需要将维护下凸壳改为维护上凸壳。具体方法是把两个 cmp
函数里判断大小的符号改一下即可。
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <bits/stdc++.h> using namespace std;
typedef long long ll; ll n,a,b,c,s[1000005],dp[1000005],q[1000005],l=1,r=0; ll Y(ll x){return dp[x]+a*s[x]*s[x]-b*s[x];} ll K(ll x){return 2*a*s[x];} ll X(ll x){return s[x];} bool cmp1(ll p1,ll p2,ll kk){ ll x=X(p1),y=Y(p1),x2=X(p2),y2=Y(p2); return (y2-y)>kk*(x2-x); } bool cmp2(ll p1,ll p2,ll p3){ ll x=X(p1),y=Y(p1),x2=X(p2),y2=Y(p2),x3=X(p3),y3=Y(p3); return (y2-y)*(x3-x2)<(y3-y2)*(x2-x); } int main(){ scanf("%lld%lld%lld%lld",&n,&a,&b,&c); q[++r]=0; for(ll i=1;i<=n;i++) scanf("%lld",&s[i]),s[i]+=s[i-1]; for(ll i=1;i<=n;i++){ while(l<r&&cmp1(q[l],q[l+1],K(i))) l++; ll j=q[l]; dp[i]=dp[j]+a*(s[i]-s[j])*(s[i]-s[j])+b*(s[i]-s[j])+c; while(l<r&&cmp2(q[r-1],q[r],i)) r--; q[++r]=i; } printf("%lld\n",dp[n]); return 0; }
|