$$dp[j_2]+S_i^2+S_{j_2}^2+M^2-2S_iS_{j_2}-2S_iM+2S_{j_2}M \leq dp[j_1]+S_i^2+S_{j_1}^2+M^2-2S_iS_{j_1}-2S_iM+2S_{j_1}M$$
$$\Leftrightarrow dp[j_2]+S_{j_2}^2-2S_iS_{j_2}+2S_{j_2}M \leq dp[j_1]+S_{j_1}^2-2S_iS_{j_1}+2S_{j_1}M$$
整理得:
$$-2S_i(S_{j_1}-S_{j_2}) \geq (dp[j_2]+S_{j_2}^{2}+2S_{j_2}M)-(dp[j_1]+S_{j_1}^{2}+2S_{j_1}M)$$
即:
$$2S_i(S_{j_1}-S_{j_2}) \geq (dp[j_1]+S_{j_1}^{2}+2S_{j_1}M)-(dp[j_2]+S_{j_2}^{2}+2S_{j_2}M)$$
$$\because C_i \geq 1,j_1 \neq j_2$$
$$\therefore S_{j_1} \neq S_{j_2}$$
$$\therefore 2S_i \geq \dfrac{(dp[j_1]+S_{j_1}^{2}+2S_{j_1}M)-(dp[j_2]+S_{j_2}^{2}+2S_{j_2}M)}{S_{j_1}-S_{j_2}}$$
令 $X(j)=S_{j},Y(j)=dp[j]+S_{j}^{2}+2S_{j}M$,得:
$$2S_{i} \geq \dfrac{Y(j_1)-Y(j_2)}{X(j_1)-X(j_2)}$$
满足此条件时有 $j_2$ 优于 $j_1$,则根据 _原理_ 处的推导,只需要维护 $j \in [1,i-1]$ 对应的点集 $(X(j),Y(j))$ 的下凸包即可。
又由于 $S_i$ 单调递增,所以状态转移具有决策单调性,可以用单调队列维护,已经不是 $i$ 的最优决策点的点不会再是 $i+1$ 的最优决策点(关键在于自己结合前面的式子思考一下)。
注意:
- 单调队列中维护凸包上的点对应的 $j$。
- 维护的点不应存在三点共线,而应当只维护两端点。
- 单调队列中应保证至少有两个点再求斜率,deque 难以实现且常数大,建议使用手写队列。
- 维护凸包比较斜率时建议不要使用除法,容易被卡精度,交叉相乘是更好的选择。同时, $\dfrac{a}{0} \geq \dfrac{b}{c}$ 交叉相乘后恒成立,这难道不正是我们一直在找的 $\dfrac{a}{0}=\infty$ 的可行实现方案吗?
做完这一通,你就会发现:solve 1 推式子还可以,但是上 or 下凸包的分析不(你)是(得)很(自己)直(画)观(图),优势是推出来的东西直接就能写代码,写起来很方便。
### AC CODE
```cpp
#include<bits/stdc++.h>
using namespace std;
int n;
long long M;
long long a[100005];
long long sum[100005];
long long dp[100005];
int q[100005];
long long X(int j){
return sum[j];
}
long long Y(int j){
return dp[j]+sum[j]*sum[j]+2*sum[j]*M;
}
long long up(int i,int j){
return Y(j)-Y(i);
}
long long down(int i,int j){
return X(j)-X(i);
}
int main(){
cin>>n>>M;
M++;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i]+1;
}
int h=0,t=0;
q[t]=0;
for(int i=1;i<=n;i++){
while(h<t&&2*sum[i]*down(q[h],q[h+1])>=up(q[h],q[h+1])) h++;
dp[i]=dp[q[h]]+(sum[i]-sum[q[h]]-M)*(sum[i]-sum[q[h]]-M);
while(h<t&&up(q[t],i)*down(q[t-1],q[t])<=up(q[t-1],q[t])*down(q[t],i)) t--;
q[++t]=i;
}
cout<<dp[n]<<"\n";
return 0;
}
```
## [AT_dp_z Frog 3](https://www.luogu.com.cn/problem/AT_dp_z)
### 分析
依旧是设 $dp[i]$ 表示跳到第 $i$ 块石头上的最小花费。
题目里给出了状态转移方程:
$$dp[i]=\min_{j=1}^{i-1}((h_i-h_j)^2+C)$$
又是斜优没跑了……这里用 solve 2 里的方法推一下式子。
### 暴推式子
把 $\min$ 去掉,平方拆开:
$$dp[i]=h_i^2+h_j^2-2h_{i}h_{j}+C$$
按照上文说的,将只含 $j$ 的项放一边:
$$h_j^2=2h_{i}h_{j}+dp[i]-h_i^2-C$$
令 $y_j=h_j^2,k=2h_i,x_j=h_j,b=dp[i]-h_i^2-C$,这又变成了一个过 $(x_j,y_j)$ 的一次函数解析式求 $b$ 最小值的问题,依旧是维护下凸包,将 $j$ 抽象成平面直角坐标系上的点 $(x_j,y_j)$ 即可。
本题保证 $h_i$ 单调递增,故具有决策单调性,单调队列维护下凸包即可,当前位置 $i$ 的最佳决策点是下凸包上第一个斜率大于 $h_i$ 的直线的左端点(最后一个斜率小于等于 $h_i$ 的直线的右端点),图像想起来很直观。
solve 2 这样式子推得很直观,图像(上 or 下凸包)想起来也很方便,但是体现在代码语言里的就太少了,写代码的时候还得参考一下 solve 1。
### AC CODE
```cpp
#include<bits/stdc++.h>
using namespace std;
int n;
long long M;
long long a[200005];
long long dp[200005];
int q[200005];
long long X(int j){
return a[j];
}
long long Y(int j){
return dp[j]+a[j]*a[j];
}
long long up(int i,int j){
return Y(j)-Y(i);
}
long long down(int i,int j){
return X(j)-X(i);
}
int main(){
cin>>n>>M;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int h=0,t=0;
q[t]=1;
for(int i=2;i<=n;i++){
while(h<t&&2*a[i]*down(q[h],q[h+1])>=up(q[h],q[h+1])) h++;
dp[i]=dp[q[h]]+(a[i]-a[q[h]])*(a[i]-a[q[h]])+M;
while(h<t&&up(q[t],i)*down(q[t-1],q[t])<=up(q[t-1],q[t])*down(q[t],i)) t--;
q[++t]=i;
}
cout<<dp[n]<<"\n";
return 0;
}
```
## 任务安排全家桶
### 题目链接
#### [P2365 任务安排 1](https://www.luogu.com.cn/problem/P2365)
#### [P10979 任务安排 2](https://www.luogu.com.cn/problem/P10979)
#### [P5785 任务安排 3([SDOI2012] 任务安排 )](https://www.luogu.com.cn/problem/P5785)
### 前言
这三个题只在数据范围略有不同,~~我是不会告诉你第三题的代码能过所有的~~,根据不同的数据我们要选择合适的做法。
### 分析
先设 $dp[i]$ 表示已经完成了前 $i$ 个任务的花费。你会发现,题目中的 $s$ 难以进行处理,这里用到一个很典的思路,统计当前对后面的影响(后面难以统计前面的贡献,那么就在前面对后面有贡献的时候直接累加在前面的值上),状态的定义就这样略有变化。
再前缀和优化一下,$st[i]=\sum_{j=1}^{i} t_j,sc[i]=\sum_{j=1}^{i} c_j$。
这样一来状态转移就相当简单了:
$$dp[i]=\min_{j=1}^{i-1}(dp[j]+st[i] \times (sc[i]-sc[j])+s \times (sc[n]-sc[j]))$$
表示在 $j$ 和 $j+1$ 之间分开,第 $i$ 件物品做完,不计算 $s$ 的截止时间 $st[i]$ 乘上每个物品的花费。同时完成 $j+1$ 到 $n$ 的总时长都要在 $st$ 数组的基础上再加上这一次启动机器的时间 $s$,还要再额外花费 $s \times (sc[n]-sc[j])$。
这样的时间复杂度是 $O(n^2)$ 的,你会发现可以过掉[P2365 任务安排 1](https://www.luogu.com.cn/problem/P2365)。
注意:$0$ 和 $1$ 之间可以进行分割,$j$ 从 $0
开始枚举。
code of P2365 任务安排 1
#include<bits/stdc++.h>
using namespace std;
int n;
long long M;
long long c[300005],t[300005];
long long sc[300005],st[300005];
long long dp[300005];
int main(){
memset(dp,0x3f,sizeof dp);
cin>>n>>M;
for(int i=1;i<=n;i++){
cin>>t[i]>>c[i];
st[i]=st[i-1]+t[i];
sc[i]=sc[i-1]+c[i];
}
dp[0]=0;
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
dp[i]=min(dp[i],dp[j]+st[i]*(sc[i]-sc[j])+M*(sc[n]-sc[j]));
}
}
cout<<dp[n]<<"\n";
return 0;
}
如果将 j 在平面直角坐标系中对应的点设为 (x,y)。问题就转化成了拿斜率为 k 的线去卡每个点,最小化截距 b。
易知最优决策点是下凸包上最后一个斜率小于等于 k 的右端点。
在P10979 任务安排 2中,t_i \geq 1,所以 st[i] 单调递增,进而 k=st[i]+s 单调递增,如果下凸包上某两个点构成的直线的斜率已经不是 i 前面最大的小于等于 st[i]+s 的斜率,由于 st[i+1]+s>st[i],所以该直线的右端点也不可能作为 i+1 的最优决策点。这种特性使得本题可以使用单调队列在维护下凸包的同时维护 i 的最优决策点。
code of P10979 任务安排 2
#include<bits/stdc++.h>
using namespace std;
int n;
long long M;
long long c[300005],t[300005];
long long sc[300005],st[300005];
long long dp[300005];
int q[300005];
long long X(int j){
return sc[j];
}
long long Y(int j){
return dp[j];
}
long long up(int i,int j){
return Y(j)-Y(i);
}
long long down(int i,int j){
return X(j)-X(i);
}
int main(){
cin>>n>>M;
for(int i=1;i<=n;i++){
cin>>t[i]>>c[i];
st[i]=st[i-1]+t[i];
sc[i]=sc[i-1]+c[i];
}
int h=0,t=0;
q[t]=0;
for(int i=1;i<=n;i++){
while(h<t&&down(q[h],q[h+1])*(st[i]+M)>=up(q[h],q[h+1])) h++;
dp[i]=dp[q[h]]+st[i]*(sc[i]-sc[q[h]])+M*(sc[n]-sc[q[h]]);
while(h<t&&up(q[t],i)*down(q[t-1],q[t])<=up(q[t-1],q[t])*down(q[t],i)){
t--;
}
q[++t]=i;
}
cout<<dp[n]<<"\n";
return 0;
}
而在P5785 任务安排 3([SDOI2012] 任务安排 )中,t_i 可能为负,所以 st[i]+s 不具有单调性。我们就需要记下来 i 前面的点的下凸包上的所有点,根据凸包上斜率的单调性进行二分。
code of P5785 任务安排 3([SDOI2012] 任务安排 )
#include<bits/stdc++.h>
using namespace std;
int n;
long long M;
long long c[300005],t[300005];
long long sc[300005],st[300005];
long long dp[300005];
int q[300005];
long long X(int j){
return sc[j];
}
long long Y(int j){
return dp[j];
}
long long up(int i,int j){
return Y(j)-Y(i);
}
long long down(int i,int j){
return X(j)-X(i);
}
int binary_search(int l,int r,long long k){
int mid=0;
while(l<r){
mid=(l+r)>>1;
if(up(q[mid],q[mid+1])<=down(q[mid],q[mid+1])*k){
l=mid+1;
}else{
r=mid;
}
}
return q[l];
}
int main(){
cin>>n>>M;
for(int i=1;i<=n;i++){
cin>>t[i]>>c[i];
st[i]=st[i-1]+t[i];
sc[i]=sc[i-1]+c[i];
}
int top=1;
q[top]=0;
for(int i=1;i<=n;i++){
int j=binary_search(1,top,M+st[i]);
dp[i]=dp[j]+st[i]*(sc[i]-sc[j])+M*(sc[n]-sc[j]);
while(top>1&&up(q[top],i)*down(q[top-1],q[top])<=up(q[top-1],q[top])*down(q[top],i)){
top--;
}
q[++top]=i;
}
cout<<dp[n]<<"\n";
return 0;
}