学习NOTE 9——斜率优化dp
dp好难。。。(还好吧)
是什么
斜率优化dp,重点是 “斜率”,想想什么东西会有斜率,函数吧,一般都是一次函数 (谁会出二次函数这种恶心东西)。
那么这时候的dp转移方程就可以优化作(看作)一次函数,然后通过推出来的公式来求\(x,y,k\)(\(b\)是常数,不过也有要求b的情况),得出当前的结果。
所以我们可以得出,斜率优化是用来优化dp的转移方程的。
怎么实现
此乃重点!
因为教练说了,代码很好写,只是理解有点难 (只是“只是,有点”吗)。
这次我们从题入手,先放一下PID:\(\color{Cyan}HDU3507\)。
(你发现它可以点击了吗)
额,是英文啊(擦汗),翻译。
首先,知道这是dp没问题吧。
因为题目要求把数分成很多段,然后求 \((\sum C_i)^2+M\)的最小值,想来想去也只有dp最合适(?)。
那就先设计状态、推转移方程吧。
基础的状态:令 \(dp_i\) 为:到第 \(i\) 个时(分了很多段,以 \(i\) 为当前段的终点),前 \(i\) 个的总费用的最小值。
那么转移方程就是这个 ↓:
解释一下,\(dp_j\) 上一段的最优解,然后 \((\sum_{i=j+1}^{i} C_i)^2+M\) 就是题目中的公式(当前这一段的)。
那么,开写!然后,T飞了(TLE),低头一看:\(0\le N\le 5\times 10^5\),这不炸鼠也得炸个半鼠 (呵呵)。这时间复杂度太超标了。
其次,优化(主要内容):
这坨求和 \((\sum_{i=j+1}^{i} C_i)^2\) 可以用前缀和优化,就成了 \(p_i-p_j\),\(O(1)\) 搞定。然后好像没什么可以优化的了。额。
那就拆开它:
然后你会发现,好像没什么能继续的了,啊呸,是 \(\min\) 可以用单调队列优化掉,然后就成了(\(j\) 是当前最优的起点):
然后就到了现在的重中之重,到了斜率优化的部分了!
我们可以把转移方程看成一次函数:
把与 \(j\) 相关的当成变量\(x,y\),将与 \(i\) 相关的当成常量 \(b\) 或者比例系数(斜率)\(k\)。不过,为什么?因为这时的 \(i\) 是固定的、已知的,而 \(j\) 是不固定的、未知的。
那就可以变成这样:
拆分一下,看看都是干什么的:

(工具:Excalidraw Whiteboard)
其实可以这样看,把它换一下:
这样才是我们要的,但是因为我们要求 \(dp_i\),所以先拿出来。
注意观察,\(b\) 才是主要影响 \(dp_i\) 的值的。但因为 \(dp_i\) 未知,所以 \(b\) 也是未知的,那么 \(b\) 越小,\(dp_i\)就越小。
那该怎么算呢?先把方程变一下:
然后再看,这时我们只需要保证 \(k\) 最大就行了,因为一个点的 \((x,y)\) 是固定的。
接下来,引进一个新的东西:凸壳
什么是凸壳

这是下凸。
我们以下凸为例,两个函数:\(y_1=kx_1+b_1\) 和 \(y_2=kx_2+b_2\)(图在上面的框里面,结合图片食用更佳)。
这时 \(y_1-kx_1\ge y_2-kx_2\),那么 \(b_1\ge b_2\),这时 \((x_2,y_2)\) 和 \((x_3,y_3)\) 的更优。
那么现在,单调队列派上用场了,用它来记录所选的这一段中,最优的点的下表 \(i\)。
根据上面的式子,我们的第一个判断条件出来了(上面单调队列的):若当前点与队尾的点所连成的直线比队尾的两个连成的更优(斜率更大),则 pop_back,
还有,既然是单调队列,还有队首的维护,这也很容易知道:若队首的斜率比当前的点的更大,则 pop_front。
比如说像这样:

这时的 \(k_3\) 是最优的。
最后,回到这道题,公式已经被“肢解”的差不多了,我们来看一下:
- 首先,\((\sum_{i=j+1}^{i} C_i)^2\) 用前缀和优化掉;
- 其次,\(\min\) 的 \(j\) 用单调队列优化掉;
- 最后,根据公式和用数学中的方法计算出结果。
那这时间已经没有多少了,(我不会算时间复杂度),接下来就是代码的事了。
代码
- 首先,前缀和优化:
for(int i=1;i<=n;i++){
cin>>p[i],p[i]+=p[i-1];
dp[i]=0;//多测不清空,__________
}
- 求 dp的同时求单调队列:
int h=1,t=0;
for(int i=1;i<=n;i++){
while(t-h+1>=2&&slope(i-1,q[t])<=slope(q[t],q[t-1])) t--;//对尾的的斜率比当前的更小,b更大
q[++t]=i-1;
while(t-h+1>=2&&slope(q[h+1],q[h])<=_k(i)) h++;//队首的斜率更小,b更大
int j=q[h];
dp[i]=_y(j)-_k(i)*_x(j)+p[i]*p[i]+m;
}
- 数学 + 公式的 \(x,y,k\):
int _x(int a){return p[a];}
int _y(int a){return dp[a]+p[a]*p[a];}
int _k(int a){return 2*p[a];}
//这三个以公式为准
double slope(int a,int b){
int x=_x(a),x2=_x(b),y=_y(a),y2=_y(b);
if(x==x2) return (y-y2)*1.0/1e-9;
return (y-y2)*1.0/(x-x2);
}
ok啦,下面是全代码:
$\color{Cyan}Code$
$\color{Red}注意是多测!!$#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+10;
int n,m,p[N],dp[N],q[N];
//q记录凸点
int _x(int a){return p[a];}
int _y(int a){return dp[a]+p[a]*p[a];}
int _k(int a){return 2*p[a];}
double slope(int a,int b){
int x=_x(a),x2=_x(b),y=_y(a),y2=_y(b);
if(x==x2) return (y-y2)*1.0/1e-9;
return (y-y2)*1.0/(x-x2);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
while(cin>>n>>m){
for(int i=1;i<=n;i++){
cin>>p[i],p[i]+=p[i-1];
dp[i]=0;
}
int h=1,t=0;
for(int i=1;i<=n;i++){
while(t-h+1>=2&&slope(i-1,q[t])<=slope(q[t],q[t-1])) t--;//对尾的的斜率比当前的更小
q[++t]=i-1;
while(t-h+1>=2&&slope(q[h+1],q[h])<=_k(i)) h++;//队首的斜率更小,b更大
int j=q[h];
dp[i]=_y(j)-_k(i)*_x(j)+p[i]*p[i]+m;
}
cout<<dp[n]<<"\n";
}
return 0;
}
TIPS
在单调队列维护凸壳的时候,注意看题目是上凸还是下凸,来修改两个 \(slope\) 的判断,是 \(\le\) 还是 \(\ge\) (你猜我是怎么知道的)。
比如说:
现在是下凸,那就是 \(\le\),因为是要判断最小值,那就是这样:
slope(i-1,q[t])<=slope(q[t],q[t-1])
slope(q[h+1],q[h])<=_k(i)
如果是上凸,那就是 \(\ge\):
slope(i-1,q[t])>=slope(q[t],q[t-1])
slope(q[h+1],q[h])>=_k(i)
完结撒花~~

浙公网安备 33010602011771号