超详细的斜率优化动态规划入门(图文说明附例题)

斜率优化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=xcxbycyb,kcd=xdxcydyc,kbd=xdxbydyb。如果 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=(rili+j=liriCjL)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+(ij1+k=j+1iCkL)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=sumi1+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+(ij1+sumisumjL)2}=min{fj+((i+sumi)(sumj+j)L1)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+ti22ti(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+ti22ti(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)+fiti2,则维护的是点集 ( 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+1j1pk(xkxj)},也就是 i,j 建立仓库, i + 1 i+1 i+1 j − 1 j-1 j1 的之间的物品都需要运往 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+pixi,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+1dpj(pi+1pj)xj

这么化简是因为先存了 p ∗ x p*x px 的前缀和与 p 的前缀和,那么可以先计算出 i+1 到 j-1 区间内的点到 n 号点的所有运输费用 d p i + 1 − d p j dp_{i+1}-dp_{j} dpi+1dpj,本来是算到 j 号点的,现在多算了一部分则需要减去,减去的部分为当前区间所有的点都会减少从 j 到 n 的这一段距离,为 ( p i + 1 − p j ) x j (p_{i+1}-p_{j})x_{j} (pi+1pj)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} fjdpj+pjxj=pi+1xj+ficidpi+1,那么维护的点集为 ( x j , f j − d p j + p j x j ) (x_{j},f_{j}-dp_{j}+p_{j}x_{j}) (xj,fjdpj+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,k1+(sk+ti)(cicj),因为时间结束是固定的,取决于前 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,k1skcj=cjti+fi,kskcitici,那么维护的点集为 ( c j , f j , k − 1 − s ∗ k ∗ c j ) (c_{j},f_{j,k-1}-s*k*c_{j}) (cj,fj,k1skcj),斜率为 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(cicj)+s(cncj),则转化式子为:

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} fjscj=ticj+fiscntici,维护的点集为 ( c j , f j − s ∗ c j ) (c_{j},f_{j}-s*c_{j}) (cj,fjscj) ,斜率为 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;
} 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值