斜率优化DP
声明
本文的证明均为伪证明,配合图像讲解,简单易懂,外加几道例题讲解,请放心食用。
引入
在形如 f [ i ] = m i n { f [ j ] + a [ j ] + b [ j ] ∗ c [ i ] + d [ i ] } f[i]=min\{f[j]+a[j]+b[j]*c[i]+d[i]\} f[i]=min{f[j]+a[j]+b[j]∗c[i]+d[i]} 的式子(也就是式子中含 i 有关项,无法简单提出到外边,但满足接下来介绍的斜率优化)。
暴力:当然可以枚举每个 j (j<i)求最小值,但复杂度是 O ( n 2 ) O(n^2) O(n2) 的。
考虑优化:可以化简成 f [ i ] = m i n { f [ j ] + a [ j ] − b [ j ] ∗ c [ i ] } + d [ i ] f[i]=min\{f[j]+a[j]-b[j]*c[i]\}+d[i] f[i]=min{f[j]+a[j]−b[j]∗c[i]}+d[i]。设 j 为最优点,则有 f [ j ] + a [ j ] = b [ j ] ∗ c [ i ] + ( f [ i ] − d [ i ] ) f[j]+a[j]=b[j]*c[i]+(f[i]-d[i]) f[j]+a[j]=b[j]∗c[i]+(f[i]−d[i]),那么将其映射到二维坐标系上,取 x,y 轴分别为 ( b [ j ] , f [ j ] + a [ j ] ) (b[j],f[j]+a[j]) (b[j],f[j]+a[j]) ,那么就相当于二维坐标系中过点 ( b [ j ] , f [ j ] + a [ j ] ) (b[j],f[j]+a[j]) (b[j],f[j]+a[j]),在斜率为 c [ i ] c[i] c[i] 时满足在 y 轴的截距最小,则 f [ i ] − d [ i ] f[i]-d[i] f[i]−d[i] 最小,可得最小的 f [ i ] f[i] f[i]。(以上对于取最大同理),所以我们要找到就是在指定斜率 c [ i ] c[i] c[i] 时过某点的最小截距(请记住这句话)。
求解
现在考虑维护最小值:
原理:对于一张二维图上的点,需要将一些无关的点剔除,因为它们对答案无贡献。
现在已经有 A-E 这五个点。观察 B,C,D 这三个点。发现如果在指定某个斜率(黄线)时过 B 在 y 轴截距是最小的,绕 B 逆时针旋转,也就是斜率逐渐增大的话,中间是不存在过 C 的情况,因为会被 D 点卡住,D 此时 y 轴的斜率才是最小的。那么可以发现对于所要求任意斜率下在 y 轴的最小截距,是不可能 C 点继承答案的因此就可以把 C 从枚举的状态转移的集合中去掉,也就是转移时不考虑 C 对答案的贡献(毕竟这个点也无法对答案贡献最优解)。
考虑如何维护可以转移的点集,发现比如 BC 斜率大于 CD 时,C 是不应该在点集里的(其实是个下凸壳,如果求最大值则是个上凸壳)。
伪证明:
设 B ( x b , y b ) , C ( x c , y c ) , D ( x d , y d ) , k b c = y c − y b x c − x b , k c d = y d − y c x d − x c , k b d = y d − y b x d − x b B(x_{b},y_{b}),C(x_{c},y_{c}),D(x_{d},y_{d}),k_{bc}=\frac{y_{c}-y_{b}}{x_{c}-x_{b}},k_{cd}=\frac{y_{d}-y_{c}}{x_{d}-x_{c}},k_{bd}=\frac{y_{d}-y_{b}}{x_{d}-x_{b}} B(xb,yb),C(xc,yc),D(xd,yd),kbc=xc−xbyc−yb,kcd=xd−xcyd−yc,kbd=xd−xbyd−yb。如果 k b c > k c d k_{bc}>k_{cd} kbc>kcd,则 k b c > k b d k_{bc}>k_{bd} kbc>kbd (如图),若指定斜率 c [ i ] > k b c > k b d c[i]>k_{bc}>k_{bd} c[i]>kbc>kbd ,则过 B,C,D 三点的最小截距由 D 来决定。
若指定斜率 c [ i ] < k b d < k b c c[i]<k_{bd}<k_{bc} c[i]<kbd<kbc,则过 B,C,D 三点的最小截距由 B 决定。
也就是说斜率只看 k b d k_{bd} kbd,至于 k b c k_{bc} kbc 什么的,都不涉及考虑。那其实就是维护一个下凸壳,每次计算结束后再插入该点即可。下凸壳的维护可以使用单调栈,也可以单调队列来维护。后文均采用单调队列维护,因为栈有的双端队列都有,而且双端队列还可以实现单调队列优化(后文提到)。
最优值点
假设目前已维护出一个可供转移的下凸壳,那么我们每次只要遍历整个下凸壳去求解最优值点的位置即可。但是这么做的单次遍历复杂度是可以达到 O ( n ) O(n) O(n) 的。
二分优化
观察下凸壳每条边的性质,可以发现从左往右,每条边的斜率呈单调递增,这给了我们二分的可能性。
先给定当前所要求在斜率为 c [ i ] c[i] c[i] 时,过其中任意点与 y 轴具有最小截距。
给定斜率为 c [ i ] c[i] c[i] 的直线,从最下方逐渐上移直到碰到凸壳的一个点即为最优值点。如图,此时 k b d < c [ i ] < k d e k_{bd}<c[i]<k_{de} kbd<c[i]<kde,那么只需要在众多点中二分到刚好满足这关系的点,点集之间的斜率单调递增,如 k a b < k b d < c [ i ] k_{ab}<k_{bd}<c[i] kab<kbd<c[i],则说明最优质点在 D 及其右边,反过来同理,取中间点去二分到最后的点即可。因此只需要在当前单调队列存的凸壳里二分。
单调队列优化
从二分优化可知,每次是在一个下凸壳上找到一个点与前面点的斜率小于当前 c [ i ] c[i] c[i],而与后面点的斜率大于当前 c [ i ] c[i] c[i]。如果给定 c [ i ] c[i] c[i] 是递增的话,那么每次二分得出的最优值点只会往后移动,这时可以维护队首的点与下一个点的斜率是否大于当前 c [ i ] c[i] c[i],若是小与则弹出该点,因为需要找到第一个点与后面的点斜率大于 c [ i ] c[i] c[i],所以队首即为最优值点,可以做到 O ( 1 ) O(1) O(1) 查询,且每个点只会插入,弹出一次,也是 O ( 1 ) O(1) O(1) 的。
附上我的deque二分板子
struct deq{
static constexpr int N=3e5+5;
int n,hd,tl;
vector<P>dq;
deq(int x=N):n(2*x+5),hd(x+1),tl(x+1),dq(n){}
P operator[](int x){
return dq[hd+x-1];
}
int size(){
return tl-hd;
}
P front(){
return dq[hd];
}
P back(){
return dq[tl-1];
}
void push_front(P a){
dq[--hd]=a;
}
void push_back(P a){
dq[tl++]=a;
}
void pop_front(){
hd++;
}
void pop_back(){
tl--;
}
P get(ll p){//min
int l=hd,r=tl-1;
while(l<r)
{
int mid=l+r>>1;
int md=mid+1;
ll a=dq[mid].first,b=dq[mid].second;
ll c=dq[md].first,d=dq[md].second;
if(d-b>p*(c-a))r=mid;
else l=md;
}
return dq[l];
}
};
经典例题
玩具装箱
链接:P3195 [HNOI2008]玩具装箱 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意:给定 n 个物品和 L 值,每个玩具具有长度为 C i C_{i} Ci,可以将其分别多个区间 [ l i , r i ] [l_{i},r_{i}] [li,ri],每个区间的值为 a n s i = ( r i − l i + ∑ j = l i r i C j − L ) 2 ans_{i}=(r_{i}-l_{i}+\sum_{j=l_{i}}^{r_{i}}C_{j}-L)^2 ansi=(ri−li+∑j=liriCj−L)2。要求 ∑ a n s i \sum ans_{i} ∑ansi 最小,没有限制区间个数,但一个物品属于且仅属于一个区间。
题解:
设转移数组 f, f i f_{i} fi 代表到位置 i 时的最小值。则针对上式子我们可以知道 f i = min { f j + ( i − j − 1 + ∑ k = j + 1 i C k − L ) 2 } f_{i}=\min\{f_{j}+(i-j-1+\sum_{k=j+1}^{i}C_{k}-L)^2\} fi=min{fj+(i−j−1+∑k=j+1iCk−L)2}(注意 j 物品是上一个区间的)。因此对于每个 i 我们只需要找到 j ( j < i ) j(j<i) j(j<i) 的点使得 f i f_{i} fi 最小即可。单次暴力寻找的复杂度是 O ( n ) O(n) O(n) 的,总体复杂度是 O ( n 2 ) O(n^2) O(n2) 的 ,对于大数据而言是远远不足的。
考虑对式子进行化简:
设 s u m i = s u m i − 1 + C i sum_{i}=sum_{i-1}+C_{i} sumi=sumi−1+Ci,则 f i = min { f j + ( i − j − 1 + s u m i − s u m j − L ) 2 } = min { f j + ( ( i + s u m i ) − ( s u m j + j ) − L − 1 ) 2 } f_{i}=\min\{f_{j}+(i-j-1+sum_{i}-sum_{j}-L)^2\}=\min\{f_{j}+((i+sum_{i})-(sum_{j}+j)-L-1)^2\} fi=min{fj+(i−j−1+sumi−sumj−L)2}=min{fj+((i+sumi)−(sumj+j)−L−1)2}
设 t i = s u m i + i t_{i}=sum_{i}+i ti=sumi+i,则 f i = min { f j + ( t i − ( t j + L + 1 ) ) 2 } = min { f j + t i 2 − 2 t i ( t j + L + 1 ) + ( t j + L + 1 ) 2 } f_{i}=\min\{f_{j}+(t_{i}-(t_{j}+L+1))^2\}=\min\{f_{j}+t_{i}^2-2t_{i}(t_{j}+L+1)+(t_{j}+L+1)^2\} fi=min{fj+(ti−(tj+L+1))2}=min{fj+ti2−2ti(tj+L+1)+(tj+L+1)2}
对于 f i = f j + t i 2 − 2 t i ( t j + L + 1 ) + ( t j + L + 1 ) 2 f_{i}=f_{j}+t_{i}^2-2t_{i}(t_{j}+L+1)+(t_{j}+L+1)^2 fi=fj+ti2−2ti(tj+L+1)+(tj+L+1)2,将其转化为 f j + ( t j + L + 1 ) = 2 t i ( t j + L + 1 ) + f i − t i 2 f_{j}+(t_{j}+L+1)=2t_{i}(t_{j}+L+1)+f_{i}-t_{i}^2 fj+(tj+L+1)=2ti(tj+L+1)+fi−ti2,则维护的是点集 ( 2 ( t j + L + 1 ) , f j + ( t j + L + 1 ) ) (2(t_{j}+L+1),f_{j}+(t_{j}+L+1)) (2(tj+L+1),fj+(tj+L+1)) 时当斜率为 t i t_{i} ti 时的最小值。由于 t i t_{i} ti 是单调递增的,则可以用单调队列来维护,不需要在单调队列上二分查找,复杂度是 O ( n ) O(n) O(n) 的。
维护的是最小值,因此维护一个下凸壳即可。
#pragma G++ optimize("Ofast")
#pragma G++ optimize("unroll-loops")
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#include<functional>
#include<queue>
#include<unordered_map>
#include<map>
#include<set>
using namespace std;
using ll=long long;
using P=pair<ll,ll>;
const ll inf=1e18;
void solve()
{
ll n,L;
cin>>n>>L;
vector<ll>t(n+1),f(n+1);
auto pow2=[&](ll a){
return a*a;
};
for(int i=1;i<=n;i++)
{
ll j; cin>>j;
t[i]=t[i-1]+j;
}
for(int i=1;i<=n;i++)t[i]+=i;
deque<P>dq;
dq.push_front({2*(L+1),pow2(L+1)});
for(int i=1;i<=n;i++)
{
while(dq.size()>1)
{
auto[a,b]=dq.front(); dq.pop_front();
auto[c,d]=dq.front();
if(d-b>t[i]*(c-a))
{
dq.push_front({a,b}); break;
}
}
auto[a,b]=dq.front();
f[i]=b-a*t[i]+pow2(t[i]);
ll p=2*(t[i]+L+1),q=pow2(t[i]+L+1)+f[i];
while(dq.size()>1)
{
auto[c,d]=dq.back(); dq.pop_back();
auto[a,b]=dq.back();
if((d-b)*(p-c)<(q-d)*(c-a))
{
dq.push_back({c,d}); break;
}
}
dq.push_back({p,q});
}
cout<<f[n]<<"\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t=1; //cin>>t;
while(t--)solve();
return 0;
}
仓库建设
链接:P2120 [ZJOI2007]仓库建设 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意:给定 n 个工厂,每个工厂都要往最近的存储仓运输所有的东西。每个工厂位置建立存储仓的代价为 c i c_{i} ci,每个工厂有的物品数量为 p i p_{i} pi,且每个工厂与 1 号工厂的距离为 x i x_{i} xi。求最小的代价使所有物品都能运往存储仓,注意物品只能由一个小下标的工厂运往大下标的工厂或自己。运输费用一个物品运送单位距离为 1 。
题解:首先把 x 数组修改成每个点距离点 n 的距离,设 f 数组表示 f i f_{i} fi 为从 n 往 i 计算到 i 时的最小价值,且 i 建立了存储仓。则 f i = min { f [ j ] + c i + ∑ k = i + 1 j − 1 p k ∗ ( x k − x j ) } f_{i}=\min\{f[j]+c_{i}+\sum_{k=i+1}^{j-1}p_{k}*(x_{k}-x_{j})\} fi=min{f[j]+ci+∑k=i+1j−1pk∗(xk−xj)},也就是 i,j 建立仓库, i + 1 i+1 i+1 到 j − 1 j-1 j−1 的之间的物品都需要运往 j 号点,然后价值和取其中最小。
依然暴力遍历每个 j 的话总的复杂度是 O ( n 2 ) O(n^2) O(n2) 的,因此需要转换一下式子:
设 d p i = d p i + 1 + p i ∗ x i , p i + = p i + 1 dp_{i}=dp_{i+1}+p_{i}*x_{i},p_{i}+=p_{i+1} dpi=dpi+1+pi∗xi,pi+=pi+1,这样可以把问题转化为: f i = f j + c i + d p i + 1 − d p j − ( p i + 1 − p j ) ∗ x j f_{i}=f_{j}+c_{i}+dp_{i+1}-dp_{j}-(p_{i+1}-p_{j})*x_{j} fi=fj+ci+dpi+1−dpj−(pi+1−pj)∗xj
这么化简是因为先存了 p ∗ x p*x p∗x 的前缀和与 p 的前缀和,那么可以先计算出 i+1 到 j-1 区间内的点到 n 号点的所有运输费用 d p i + 1 − d p j dp_{i+1}-dp_{j} dpi+1−dpj,本来是算到 j 号点的,现在多算了一部分则需要减去,减去的部分为当前区间所有的点都会减少从 j 到 n 的这一段距离,为 ( p i + 1 − p j ) x j (p_{i+1}-p_{j})x_{j} (pi+1−pj)xj,注意目前 p 代表的已经是原来 p 数组的前缀和。
接着对式子进一步写成二维坐标系的形式, f j − d p j + p j x j = p i + 1 x j + f i − c i − d p i + 1 f_{j}-dp_{j}+p_{j}x_{j}=p_{i+1}x_{j}+f_{i}-c_{i}-dp_{i+1} fj−dpj+pjxj=pi+1xj+fi−ci−dpi+1,那么维护的点集为 ( x j , f j − d p j + p j x j ) (x_{j},f_{j}-dp_{j}+p_{j}x_{j}) (xj,fj−dpj+pjxj),求 f i f_{i} fi 的最小值则维护下凸壳即可。同样因为斜率 p i + 1 p_{i+1} pi+1 呈单调递增,则可以单调队列维护就行。
注意:因为 f i f_{i} fi 代表的是在 i 号点建立存储仓的最小值,1 号点不一定建立,因此计算出 f 后需要扫一遍,看最后一个建立是在哪里比较合适。同时,n 号点也不一定需要建立存储仓,因为 p 可能为 0。
#pragma G++ optimize("Ofast")
#pragma G++ optimize("unroll-loops")
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#include<functional>
#include<queue>
#include<unordered_map>
#include<map>
#include<set>
#include<stack>
#include<cmath>
#include<bitset>
using namespace std;
using ll=long long;
using P=pair<ll,ll>;
const ll inf=1e18;
namespace IO{
char buf[1<<20],*P1=buf,*P2=buf;
#define gc() getchar()
//#define gc() (P1==P2&&(P2=(P1=buf)+fread(buf,1,1<<20,stdin),P1==P2)?EOF:*P1++)
#define TT template<class T>inline
TT void read(T&x)
{
x=0; char c=gc(); bool f=0;
while(c<48||c>57){f^=c=='-',c=gc();}
while(47<c&&c<58)x=(x<<3)+(x<<1)+(c^48),c=gc();
if(f)x=-x;
}
TT void print(T x)
{
int t[30]={0},cnt=0;
if(x<0)putchar('-'),x=-x;
while(x)t[++cnt]=x%10,x/=10;
if(!cnt)putchar('0');
else while(cnt)putchar(t[cnt--]+'0');
puts("");
}
template<class A,class ...B>inline
void read(A&x,B&...y)
{
read(x),read(y...);
}
};
using namespace IO;
void solve()
{
int n; cin>>n;
vector<ll>x(n+1),p(n+1),c(n+1);
for(int i=1;i<=n;i++)
{
read(x[i],p[i],c[i]);
// cin>>x[i]>>p[i]>>c[i];
}
for(int i=1;i<=n;i++)
{
x[i]=x[n]-x[i];
}
vector<ll>dp(n+1),f(n+1);
f[n]=p[n]?c[n]:0;
for(int i=n-1;i>=1;i--)
{
dp[i]=dp[i+1]+x[i]*p[i];
p[i]+=p[i+1];
}
deque<P>dq;
dq.push_back({x[n],f[n]-dp[n]+p[n]*x[n]});
for(int i=n-1;i>=1;i--)
{
while(dq.size()>1)
{
auto[a,b]=dq.front(); dq.pop_front();
auto[c,d]=dq.front();
if(d-b>p[i+1]*(c-a))
{
dq.push_front({a,b}); break;
}
}
auto[a,b]=dq.front();
f[i]=b-a*p[i+1]+dp[i+1]+c[i];
ll u=x[i],v=f[i]-dp[i]+p[i]*x[i];
while(dq.size()>1)
{
auto[c,d]=dq.back(); dq.pop_back();
auto[a,b]=dq.back();
if((d-b)*(u-c)<(v-d)*(c-a))
{
dq.push_back({c,d}); break;
}
}
dq.push_back({u,v});
}
ll ans=f[1];
for(int i=2;i<=n;i++)
{
ans=min(ans,f[i]+dp[1]-dp[i]-(p[1]-p[i])*x[i]);
}
print(ans);
}
int main()
{
// ios::sync_with_stdio(false);
// cin.tie(0); cout.tie(0);
int t=1;// cin>>t;
while(t--)solve();
return 0;
}
任务安排
弱化版链接:P2365 任务安排 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
强化版链接:P5785 [SDOI2012]任务安排 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题意:给定 n 个任务和开机启动时间 s ,n 个任务具有费用系数 c i c_{i} ci。同样分成多个不相交区间,每个区间一开始要从上个区间的结束时间开始算,需要加上开机时间,然后区间内所有任务的时间加起来就到达最终时刻。则区间内所有任务完成的最终时刻乘以每个任务费用系数的和。求所有任务运行完的最小费用值。
题解:首先对于弱化版我直接打出了一手 n t nt nt 的 O ( n 2 ) \ O(n^2) O(n2) 的斜率优化(其实方程写好点暴力也是 O ( n 2 ) O(n^2) O(n2) 的,优化后则为 O ( n ) O(n) O(n),等下会提到)。具体为开二维 f 数组代表前面分成了多少组的最小值。那么式子为:
设 c,t 为原数组 c,t 的前缀和,则有: f i , k = f j , k − 1 + ( s ∗ k + t i ) ∗ ( c i − c j ) f_{i,k}=f_{j,k-1}+(s*k+t_{i})*(c_{i}-c{j}) fi,k=fj,k−1+(s∗k+ti)∗(ci−cj),因为时间结束是固定的,取决于前 i 个任务的时长和与开了多少次机的时间和。然后转换式子为: f j , k − 1 − s ∗ k ∗ c j = c j t i + f i , k − s ∗ k ∗ c i − t i c i f_{j,k-1}-s*k*c_{j}=c_{j}t_{i}+f_{i,k}-s*k*c_{i}-t_{i}c_{i} fj,k−1−s∗k∗cj=cjti+fi,k−s∗k∗ci−tici,那么维护的点集为 ( c j , f j , k − 1 − s ∗ k ∗ c j ) (c_{j},f_{j,k-1}-s*k*c_{j}) (cj,fj,k−1−s∗k∗cj),斜率为 t i t_{i} ti,最小值则为下凸壳。弱化版的 t i t_{i} ti 是单调递增的,则单调队列就可以。复杂度 O ( n 2 ) O(n^2) O(n2)。
#pragma G++ optimize("Ofast")
#pragma G++ optimize("unroll-loops")
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#include<functional>
#include<queue>
#include<unordered_map>
#include<map>
#include<set>
#include<stack>
#include<cmath>
#include<bitset>
using namespace std;
using ll=long long;
using P=pair<ll,ll>;
const ll inf=1e18;
void solve()
{
ll n,s; cin>>n>>s;
vector<ll>t(n+1),c(n+1);
for(int i=1;i<=n;i++)
{
cin>>t[i]>>c[i];
t[i]+=t[i-1];
c[i]+=c[i-1];
}
vector<vector<ll>>f(n+1,vector<ll>(n+1,inf/100));
//f[i][k]=f[j][k-1]+(s*k+t[i])*(c[i]-c[j]);
for(int i=1;i<=n;i++)
{
f[i][1]=c[i]*(s+t[i]);
}
for(int k=2;k<=n;k++)
{
deque<P>dq;
dq.push_back({0,0});
for(int i=1;i<=n;i++)
{
while(dq.size()>1)
{
auto[a,b]=dq.front(); dq.pop_front();
auto[c,d]=dq.front();
if(d-b>t[i]*(c-a))
{
dq.push_front({a,b}); break;
}
}
auto[a,b]=dq.front();
f[i][k]=b-a*t[i]+s*k*c[i]+t[i]*c[i];
ll u=c[i],v=f[i][k-1]-s*k*c[i];
while(dq.size()>1)
{
auto[c,d]=dq.back(); dq.pop_back();
auto[a,b]=dq.back();
if((d-b)*(u-c)<(v-d)*(c-a))
{
dq.push_back({c,d}); break;
}
}
dq.push_back({u,v});
}
}
cout<<*min_element(f[n].begin(),f[n].end())<<"\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t=1; //cin>>t;
while(t--)solve();
return 0;
}
这样弱化版数据也可以过,但是强化版就不行了。
观察题目,其实当选择一个区间后,开机时间会对后续所有任务都有影响,则不需要计算结尾时间了,直接先将对后面的影响加上即可。可得式子为: f i = f j + t i ( c i − c j ) + s ( c n − c j ) f_{i}=f_{j}+t_{i}(c_{i}-c{j})+s(c_{n}-c_{j}) fi=fj+ti(ci−cj)+s(cn−cj),则转化式子为:
f j − s ∗ c j = t i ∗ c j + f i − s ∗ c n − t i ∗ c i f_{j}-s*c_{j}=t_{i}*c_{j}+f_{i}-s*c_{n}-t_{i}*c_{i} fj−s∗cj=ti∗cj+fi−s∗cn−ti∗ci,维护的点集为 ( c j , f j − s ∗ c j ) (c_{j},f_{j}-s*c_{j}) (cj,fj−s∗cj) ,斜率为 t i t_{i} ti,维护下凸壳,然后单调队列维护,复杂度为 O ( n ) O(n) O(n)。
#pragma G++ optimize("Ofast")
#pragma G++ optimize("unroll-loops")
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#include<functional>
#include<queue>
#include<unordered_map>
#include<map>
#include<set>
#include<stack>
#include<cmath>
#include<bitset>
using namespace std;
using ll=long long;
using P=pair<ll,ll>;
const ll inf=1e18;
namespace IO{
char buf[1<<20],*P1=buf,*P2=buf;
#define gc() getchar()
//#define gc() (P1==P2&&(P2=(P1=buf)+fread(buf,1,1<<20,stdin),P1==P2)?EOF:*P1++)
#define TT template<class T>inline
TT void read(T&x)
{
x=0; char c=gc(); bool f=0;
while(c<48||c>57){f^=c=='-',c=gc();}
while(47<c&&c<58)x=(x<<3)+(x<<1)+(c^48),c=gc();
if(f)x=-x;
}
TT void print(T x)
{
int t[30]={0},cnt=0;
if(x<0)putchar('-'),x=-x;
while(x)t[++cnt]=x%10,x/=10;
if(!cnt)putchar('0');
else while(cnt)putchar(t[cnt--]+'0');
puts("");
}
template<class A,class ...B>inline
void read(A&x,B&...y)
{
read(x),read(y...);
}
};
using namespace IO;
struct deq{
static constexpr int N=3e5+5;
int n,hd,tl;
vector<P>dq;
deq(int x=N):n(2*x+5),hd(x+1),tl(x+1),dq(n){}
int size(){
return tl-hd;
}
P front(){
return dq[hd];
}
P back(){
return dq[tl-1];
}
void push_front(P a){
dq[--hd]=a;
}
void push_back(P a){
dq[tl++]=a;
}
void pop_front(){
hd++;
}
void pop_back(){
tl--;
}
P get(ll p){//min
int l=hd,r=tl-1;
while(l<r)
{
int mid=l+r>>1;
int md=mid+1;
ll a=dq[mid].first,b=dq[mid].second;
ll c=dq[md].first,d=dq[md].second;
if(d-b>p*(c-a))r=mid;
else l=md;
}
return dq[l];
}
};
void solve()
{
ll n,s; read(n,s);
vector<ll>t(n+1),c(n+1);
for(int i=1;i<=n;i++)
{
read(t[i],c[i]);
t[i]+=t[i-1];
c[i]+=c[i-1];
}
vector<ll>f(n+1);
//f[i]=f[j]+s*(c[n]-c[j])+t[i]*(c[i]-c[j])
//f[i]=f[j]+s*c[n]-s*c[j]+t[i]*c[i]-t[i]*c[j]
//f[j]-s*c[j]=t[i]*c[j]+f[i]-s*c[n]-t[i]*c[i]
//(c[j],f[j]-s*c[j])
// deque<P>dq;
deq dq(n);
dq.push_back({0,0});
for(int i=1;i<=n;i++)
{
while(dq.size()>1)
{
auto[a,b]=dq.front(); dq.pop_front();
auto[c,d]=dq.front();
if(d-b>t[i]*(c-a))
{
dq.push_front({a,b}); break;
}
}
auto[a,b]=dq.front();
f[i]=b-a*t[i]+s*c[n]+t[i]*c[i];
ll u=c[i],v=f[i]-s*c[i];
while(dq.size()>1)
{
auto[c,d]=dq.back(); dq.pop_back();
auto[a,b]=dq.back();
if((d-b)*(u-c)<(v-d)*(c-a))
{
dq.push_back({c,d}); break;
}
}
dq.push_back({u,v});
}
print(f[n]);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t=1; //cin>>t;
while(t--)solve();
return 0;
}
发现强化版数据的 t i t_{i} ti 是存在负数的,说明斜率并不单调递增,那么此时单调队列维护队首的方式就不可行了。那么就在单调队列上二分找到最优值点即可,复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))。
#pragma G++ optimize("Ofast")
#pragma G++ optimize("unroll-loops")
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#include<functional>
#include<queue>
#include<unordered_map>
#include<map>
#include<set>
#include<stack>
#include<cmath>
#include<bitset>
using namespace std;
using ll=long long;
using P=pair<ll,ll>;
const ll inf=1e18;
namespace IO{
char buf[1<<20],*P1=buf,*P2=buf;
#define gc() getchar()
//#define gc() (P1==P2&&(P2=(P1=buf)+fread(buf,1,1<<20,stdin),P1==P2)?EOF:*P1++)
#define TT template<class T>inline
TT void read(T&x)
{
x=0; char c=gc(); bool f=0;
while(c<48||c>57){f^=c=='-',c=gc();}
while(47<c&&c<58)x=(x<<3)+(x<<1)+(c^48),c=gc();
if(f)x=-x;
}
TT void print(T x)
{
int t[30]={0},cnt=0;
if(x<0)putchar('-'),x=-x;
while(x)t[++cnt]=x%10,x/=10;
if(!cnt)putchar('0');
else while(cnt)putchar(t[cnt--]+'0');
puts("");
}
template<class A,class ...B>inline
void read(A&x,B&...y)
{
read(x),read(y...);
}
};
using namespace IO;
struct deq{
static constexpr int N=3e5+5;
int n,hd,tl;
vector<P>dq;
deq(int x=N):n(2*x+5),hd(x+1),tl(x+1),dq(n){}
P operator[](int x){
return dq[hd+x-1];
}
int size(){
return tl-hd;
}
P front(){
return dq[hd];
}
P back(){
return dq[tl-1];
}
void push_front(P a){
dq[--hd]=a;
}
void push_back(P a){
dq[tl++]=a;
}
void pop_front(){
hd++;
}
void pop_back(){
tl--;
}
P get(ll p){//min
int l=hd,r=tl-1;
while(l<r)
{
int mid=l+r>>1;
int md=mid+1;
ll a=dq[mid].first,b=dq[mid].second;
ll c=dq[md].first,d=dq[md].second;
if(d-b>p*(c-a))r=mid;
else l=md;
}
return dq[l];
}
};
void solve()
{
ll n,s; read(n,s);
vector<ll>t(n+1),c(n+1);
for(int i=1;i<=n;i++)
{
read(t[i],c[i]);
t[i]+=t[i-1];
c[i]+=c[i-1];
}
vector<ll>f(n+1);
//f[i]=f[j]+s*(c[n]-c[j])+t[i]*(c[i]-c[j])
//f[i]=f[j]+s*c[n]-s*c[j]+t[i]*c[i]-t[i]*c[j]
//f[j]-s*c[j]=t[i]*c[j]+f[i]-s*c[n]-t[i]*c[i]
//(c[j],f[j]-s*c[j])
// deque<P>dq;
deq dq(n);
dq.push_back({0,0});
for(int i=1;i<=n;i++)
{
auto[a,b]=dq.get(t[i]);
f[i]=b-a*t[i]+s*c[n]+t[i]*c[i];
ll u=c[i],v=f[i]-s*c[i];
while(dq.size()>1)
{
auto[c,d]=dq.back(); dq.pop_back();
auto[a,b]=dq.back();
if((d-b)*(u-c)<(v-d)*(c-a))
{
dq.push_back({c,d}); break;
}
}
dq.push_back({u,v});
}
print(f[n]);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int t=1; //cin>>t;
while(t--)solve();
return 0;
}