dp斜率优化

dp斜率优化

T1 hdu3507 打印文章

题目描述:

给定一个含 n 个数的数列 CnM ,将 Cn 分为若干段 [a,b] ,求所有子段的 W 之和的最小值.

Wa,b=(i=abCi)2+M

n5105M1000

思路:

(1) Si=j=1iCj(2)Wa,b=SbSa1(3) fiiW(4)fi=minj=1i1(fj+(SiSj)2)+M(5) fi=minj=1i1(fj+Si2+Sj22SiSj)+M

将只含不确定性 (j) 的项放在左边,含有 i 的项放在右边:

(6)fj+Sj2=(2Si)Sj+(fiSi2M)(7)y=kx+b(8)y=fj+Sj2k=2Six=Sjb=fiSi2M(:)

  每个决策 j 都对应着一个点 (Sj,fj+Sj2) ,目标是使纵截距 b 最小.

维护一个下凸包:

image

如图所示b取最小:

**<!!!>此时满足: kh( 线的斜率 )<k( 点的决策斜率 )<ki( 线的斜率 ) **

image

利用单调队列维护决策点.

对于任意 j1<j2 ,必有 Sj1<Sj2 ,故不存在 J 点所示情况,即新的决策点必出现在下突壳的最右端.

维护下突壳:检查新加入的点 L 和之前两点 K,E 是否满足下凸性,若不满足则弹出队尾 (K) ,然后将新决策点 (L) 入队.

随着 i 的增加,斜率 k=2Si   ,故一次查询的最优决策点之前的斜率 k 的线均无效,出队.
队头即是最优决策点.
(若 k 不满足单调递增(如 T4 ),则不能直接弹出斜率 k 的所有决策点,队头也不一定是最优决策点,要在单调队列中二分出一个点 O 使得 kOP<k<kPQ )

image

Code:

#include<bits/stdc++.h>
#define ll long long
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
#define pb push_back
#define pii pair<int,int>
#define mp(x,y) make_pair(x,y)
#define msec 800
using namespace std;
const int N=5e5+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n,m;
int s[N],dp[N];

int q[N],h,t;

inline int X(int k)
{
	return s[k];
}
inline int Y(int k)
{
	return dp[k]+s[k]*s[k];
}
inline int K_nodei(int k)
{
	return 2*s[k];
}
inline int deltaX(int k1,int k2)
{
	return X(k2)-X(k1);
}
inline int deltaY(int k1,int k2)
{
	return Y(k2)-Y(k1);
}
inline bool Kjud1(int h,int i)
{
	return bool(deltaY(q[h],q[h+1])<=K_nodei(i)*deltaX(q[h],q[h+1])); //A--a--B--b--C ,b的斜率大等于a  =>   ka<=kb[已知] Δy(AB) / Δx(AB)<=kb  =>  Δy(AB)<=kb*Δx(AB)
}
inline bool Kjud2(int h,int i)
{
	return bool(deltaY(q[t],i)*deltaX(q[t-1],q[t])<=deltaY(q[t-1],q[t])*deltaX(q[t],i)); //k2<=k1   =>   y2/x2<=y1/x1   =>   y2*x1<=y1*x2
}
inline int fi(int i,int j)
{
	return dp[j]+(s[i]-s[j])*(s[i]-s[j])+m;//回到最初的定义
}

inline void init()
{
	h=t=0;
}
int main()
{
	while(~scanf("%d%d",&n,&m))
	{
		init();
		fr(i,n) s[i]=s[i-1]+rd();
		fr(i,n)
		{
			while(h<t/*防队列退空,至少应当留有一项*/&&Kjud1(h,i)) ++h;//若前一个点到这个点的斜率<=该点的斜率要求(2*s[i]),加之斜率k单调递增,则前一个点已经无效
			dp[i]=fi(i,q[h]/*当前最优决策点*/);
			while(h<t&&Kjud2(h,i)) --t;
			q[++t]=i;
		}
		cout<<dp[n];
	}
	return 0;
}

T2&T3 #P10184.任务安排 1 P2365 任务安排 & #P10185.任务安排 2

题目描述:

给定一个含 n 个数的数列 CnTnS ,将 Cn 分为若干段 [a,b] ,求所有子段的 W 之和的最小值.

k[a,b]:T=kS+i=1bTiWk(a,b)=(i=abCi)T=(i=1bTi)(i=abCi)+kS(i=abCi)

1<n1041S501Ti,Ci100

思路:

(9) sumCi=j=1iCjsumTi=j=1iTj(10) fiiW(11)fi=minj=1i1(fj+sumTi(sumCisumCj)+S(sumCnsumCj))

<!!! 费用提前计算>↑

既然一次操作会使后面所有段的 T 均增加 SS 只与 Ci 有关,那就一次性将后面所有的 Ci 全部进行一次更新,提前计算所需费用.

(12)fi=minj=1i1(fj+sumTisumCi+SsumCnSsumCjsumTisumCj)

移项得:

(13)fjSsumCj=sumTisumCj+fisumTisumCiSsumCn(14)y=kx+b(15)y=fjSsumCjk=sumTix=sumCjb=fisumTisumCiSsumCn(:)

  每个决策 j 都对应着一个点 (sumCj,fjSsumCj) ,目标是使纵截距 b 最小,故维护一个下凸包.

T1 , k 依然满足单调递增,故可以用单调队列维护斜率 k .

T1 , x 值也依然满足单调递增.

Code:

#include<bits/stdc++.h>
#define ll long long
#define int ll
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
#define pb push_back
#define pii pair<int,int>
#define mp(x,y) make_pair(x,y)
#define msec 800
using namespace std;
const int N=3e5+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n=rd(),S=rd();
int sumC[N],sumT[N];
int q[N],h,t;
int dp[N];

inline int X(int k)
{
	return sumC[k];
}
inline int Y(int k)
{
	return dp[k]-S*sumC[k];
}
inline int K(int k)
{
	return sumT[k];
}
inline int deltaX(int k1,int k2)
{
	return X(k2)-X(k1);
}
inline int deltaY(int k1,int k2)
{
	return Y(k2)-Y(k1);
}
inline bool Kjud1(int i)
{
	return bool(deltaY(q[h],q[h+1])<=K(i)*deltaX(q[h],q[h+1]));
}
inline bool Kjud2(int i)
{
	return bool(deltaY(q[t],i)*deltaX(q[t-1],q[t])<=deltaY(q[t-1],q[t])*deltaX(q[t],i));
}
inline int Fi(int i,int j)
{
	return dp[j]+sumT[i]*(sumC[i]-sumC[j])+S*(sumC[n]-sumC[j]);
}
signed main()
{
	fr(i,n) sumT[i]=sumT[i-1]+rd(),sumC[i]=sumC[i-1]+rd();
	fr(i,n)
	{
		while(h<t&&Kjud1(i)) ++h;
		dp[i]=Fi(i,q[h]);
		while(h<t&&Kjud2(i)) --t;
		q[++t]=i;
	}
	cout<<dp[n];
	return 0;
}

T4 #P10186. 任务安排 3 P5785 [SDOI2012]任务安排

题目描述:

T2&T3

1<n31051S2828Ti280Ci100

思路:

(16)fjSsumCj=sumTisumCj+fisumTisumCiSsumCn(17)y=kx+b(18)y=fjSsumCjk=sumTix=sumCjb=fisumTisumCiSsumCn(:)

因为 Ti 可能 0 , k 不再满足单调递增,所以不能再用单调队列来维护决策点.(注意:点的 k 和线的 k 不是一个东西,因为下突壳的维护,决策点之间依然满足 k 的单调递增,不过在对每个点进行查询时 k 不单调递增,故无法顺行一次查完)
二分查找最优决策点 p ,使得 p 为满足 kp<ki 的最早的决策点(此时满足 kpkikp ).

其他过程同 T1T3

Code:

#include<bits/stdc++.h>
#define ll long long
#define int ll
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
#define pb push_back
#define pii pair<int,int>
#define mp(x,y) make_pair(x,y)
#define msec 800
using namespace std;
const int N=3e5+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n=rd(),S=rd();
int sumC[N],sumT[N];
int q[N],h,t;
int dp[N];

inline int X(int k)
{
	return sumC[k];
}
inline int Y(int k)
{
	return dp[k]-S*sumC[k];
}
inline int K(int k)
{
	return sumT[k];
}
inline int deltaX(int k1,int k2)
{
	return X(k2)-X(k1);
}
inline int deltaY(int k1,int k2)
{
	return Y(k2)-Y(k1);
}
inline bool Kjud1(int h,int i)
{
	return bool(deltaY(q[h],q[h+1])<=K(i)*deltaX(q[h],q[h+1]));//h-->h+1斜率小于点斜率
}
inline bool Kjud2(int t,int i)
{
	return bool(deltaY(q[t],i)*deltaX(q[t-1],q[t])<=deltaY(q[t-1],q[t])*deltaX(q[t],i));
}
inline int Fi(int i,int j)
{
	return dp[j]+sumT[i]*(sumC[i]-sumC[j])+S*(sumC[n]-sumC[j]);
}

inline int bin_search(int i)
{
	int l=h,r=t;//<-1 x>
	while(l<r)
	{
		int mid=(l+r)>>1;//检查线段mid-->mid+1斜率
		if(Kjud1(mid,i)) l=mid+1;
		else r=mid;//<-1 x>
	}
	return q[l];
}

int p;
signed main()
{
	fr(i,n) sumT[i]=sumT[i-1]+rd(),sumC[i]=sumC[i-1]+rd();
	fr(i,n)
	{
		p=bin_search(i);
		dp[i]=Fi(i,p);
		while(h<t&&Kjud2(t,i)) --t;
		q[++t]=i;
	}
	cout<<dp[n];
	return 0;
}

T5 #P10188. 玩具装箱 P3195 玩具装箱

题目描述:

给定一个数列 Cn 和一个常数 L ,将其分为若干 (k1) 段,对于其中一段 t=[i,j] ,

(19)Wt=[i,j]=[(ji+k=ijCk)L]2(20)W=i=1k1Wt

W 的最小值.

1n51041L1071Ci107

思路:

(21) Sn=i=1nCn(22)Wj=(ij+SiSj1L)2(23) fiiW(24)fi=minj=2i(fj1+i2+j2+)//

(25) Sn=i=1nCn+n//(26)Wj+1,i=(SiSj(L+1))2(27) fi=minj=1i1(fj+Sj2+2(L+1)Sj2SiSj+Si22(L+1)Si+(L+1)2)(28)(29)fj+Sj2+2(L+1)Sj=(2Si)Sj+fiSi2+2(L+1)Si(L+1)2(30)y=fj+Sj2+2(L+1)Sjk=2Six=Sjb=fiSi2+2(L+1)Si(L+1)2

* T1 提到的移项方法是一种通法,但一个式子可能不止一种移项方法,不同移法的 y,k,x,b 可能会有所不同,但结果相同:

(31) Sn=i=1nCn+n//(32)Wj+1,i=(SiSj(L+1))2(33) fi=minj=1i1(fj+Sj2+2(L+1)Sj2SiSj+Si22(L+1)Si+(L+1)2)(34)(35)fj+Sj2=(2Si2(L+1))Sj+fiSi2+2(L+1)Si(L+1)2(36)y=fj+Sj2k=2Si2(L+1)x=Sjb=fiSi2+2(L+1)Si(L+1)2

(还有更多方法)

本题需要维护 b 值最小,故维护一个下凸包.

其余同 T2 .

Code:

#include<bits/stdc++.h>
#define ll long long
#define int ll
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
#define pb push_back
#define pii pair<int,int>
#define mp(x,y) make_pair(x,y)
#define msec 800
using namespace std;
const int N=5e4+10,Inf=0x7fffffff;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n=rd(),L=rd();
int S[N];
int q[N],h,t;
int dp[N];

inline int X(int j)
{
	return S[j];
}
inline int Y(int j)
{
	return dp[j]+S[j]*S[j]+2*(L+1)*S[j];
}
inline int K(int i)
{
	return 2*S[i];
}
inline int deltaX(int k1,int k2)
{
	return X(k2)-X(k1);
}
inline int deltaY(int k1,int k2)
{
	return Y(k2)-Y(k1);
}
inline bool Kjud1(int i)
{
	return bool(deltaY(q[h],q[h+1])<=K(i)*deltaX(q[h],q[h+1]));
}
inline bool Kjud2(int i)
{
	return bool(deltaY(q[t],i)*deltaX(q[t-1],q[t])<=deltaY(q[t-1],q[t])*deltaX(q[t],i));
}
inline int Fi(int i,int j)
{
	return dp[j]+(S[i]-S[j]-(L+1))*(S[i]-S[j]-(L+1));
}
signed main()
{
	fr(i,n) S[i]=S[i-1]+rd()+1;
	fr(i,n)
	{
		while(h<t&&Kjud1(i)) ++h;
		dp[i]=Fi(i,q[h]);
		while(h<t&&Kjud2(i)) --t;
		q[++t]=i;
	}
	cout<<dp[n];
	return 0;
}

T6 P3648 [APIO2014]序列分割

题目描述:

你正在玩一个关于长度为 n 的非负整数序列的游戏。这个游戏中你需要把序列分成 k+1 个非空的块。为了得到 k+1 块,你需要重复下面的操作 k 次:

选择一个有超过一个元素的块(初始时你只有一块,即整个序列)

选择两个相邻元素把这个块从中间分开,得到两个非空的块。

每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分。

2n1051kmin{n1,200}0Ci104

思路:

1.收益与切割顺序无关:

image

如上图,对于 [a,c] 这一序列中的三个相邻子段而言,
先切下 a : a(b+c)+bc=ab+ac+bc
先切下 c : c(a+b)+ab=ab+ac+bc
所有的子段均遵循此规律
由是可知得分与切割顺序无关.

于是乎就可以把题目改写为:
给定一个数列 Cn ,将其分为 k+1 段,求最大的

(37)S=i=1nCn(38)W=(S2i=1k+1Ti2)/2(39)(Tii)

每一个新加入的段的收益便为该段元素和*前面的所有元素的和.

2.dp.

(40)fi,jij(41)Si=j=1iCj(42)fi,j=fz+1,j1+(SiSz)Sz

利用滚动数组节省空间,二维数组改一维.(不优化的话 320MB+ ,过是能过,但是……)

(43)fiiz(44)   fjj(z1)(45)fi=maxj=0i1(fj+SiSjSj2)(46)fjSj2=SiSj+fi(47)y=fjSj2k=Six=Sjb=fi

维护一个上凸壳.

(48):y=fj+Sj2k=Six=Sjb=fi

秒变下凸壳(后面采取此方法).

注意: ai 可能为 0 ,也就是说相邻两个决策点的 x 值可能相同, k 要特判返回 +inf .(不过像代码中那样除法转换成乘法就不必特判了)

Code1:滚优

893ms / 163.92MB (O2)

#include<bits/stdc++.h>
#define ll long long
#define int ll
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
#define pb push_back
#define pii pair<int,int>
#define mp(x,y) make_pair(x,y)
#define msec 800
using namespace std;
const int N=1e5+10,maxK=210,Inf=9223372036854775807;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n=rd(),kc=rd();
int S[N];
int q[N],h,t;
int dp[N][2];bool d;
int pre[N][maxK];

inline int X(int j)
{
	return S[j];
}
inline int Y(int j)
{
	return -dp[j][d^1]+S[j]*S[j];
}
inline int K(int i)
{
	return S[i];
}
inline int Fi(int i,int j)
{
	return dp[j][d^1]+(S[i]-S[j])*S[j];
}
inline int deltaX(int k1,int k2)
{
	return X(k2)-X(k1);
}
inline int deltaY(int k1,int k2)
{
	return Y(k2)-Y(k1);
}
inline bool Kjud1(int i)
{
	return bool(deltaY(q[h],q[h+1])<=K(i)*deltaX(q[h],q[h+1]));
}
inline bool Kjud2(int i)
{
	return bool(deltaY(q[t],i)*deltaX(q[t-1],q[t])<=deltaY(q[t-1],q[t])*deltaX(q[t],i));
}
signed main()
{
	fr(i,n) S[i]=S[i-1]+rd();
	fr(j,kc)
	{
		d^=1;//滚动数组
		h=0,t=0;
		fr(i,n)
		{
			while(h<t&&Kjud1(i)) ++h;
			dp[i][d]=Fi(i,q[h]);
			pre[i][j]=q[h];//标记前i个数切第j刀的最优位置
			while(h<t&&Kjud2(i)) --t;
			q[++t]=i;
		}
	}
	cout<<dp[n][d]<<'\n';
	int pos=n;
	Rof(i,kc,1)pos=pre[pos][i]/*返回了上次切的位置*/,cout<<pos<<' ';
	return 0;
}

Code2:无滚优

1.76s / 323.00MB (O2)

#include<bits/stdc++.h>
#define ll long long
#define int ll
#define fr(i,r) for(int i=1;i<=r;++i)
#define For(i,l,r) for(int i=l;i<=r;++i)
#define Rof(i,r,l) for(int i=r;i>=l;--i)
#define pb push_back
#define pii pair<int,int>
#define mp(x,y) make_pair(x,y)
#define msec 800
using namespace std;
const int N=1e5+10,maxK=210,Inf=9223372036854775807;
char cch;
int res,zf;
inline int rd()
{
	while((cch=getchar())<45);
	if(cch^45)res=cch^48,zf=1;
	else res=0,zf=-1;
	while((cch=getchar())>=48)res=(res*10)+(cch^48);
	return res*zf;
}

int n=rd(),kc=rd();
int S[N];
int q[N],h,t;
int dp[N][maxK],d;
int pre[N][maxK];

inline int X(int j)
{
	return S[j];
}
inline int Y(int j)
{
	return -dp[j][d-1]+S[j]*S[j];
}
inline int K(int i)
{
	return S[i];
}
inline int Fi(int i,int j)
{
	return dp[j][d-1]+(S[i]-S[j])*S[j];
}
inline int deltaX(int k1,int k2)
{
	return X(k2)-X(k1);
}
inline int deltaY(int k1,int k2)
{
	return Y(k2)-Y(k1);
}
inline bool Kjud1(int i)
{
	return bool(deltaY(q[h],q[h+1])<=K(i)*deltaX(q[h],q[h+1]));
}
inline bool Kjud2(int i)
{
	return bool(deltaY(q[t],i)*deltaX(q[t-1],q[t])<=deltaY(q[t-1],q[t])*deltaX(q[t],i));
}
signed main()
{
	fr(i,n) S[i]=S[i-1]+rd();
	fr(j,kc)
	{
		++d;
		h=0,t=0;
		fr(i,n)
		{
			while(h<t&&Kjud1(i)) ++h;
			dp[i][d]=Fi(i,q[h]);
			pre[i][j]=q[h];//标记前i个数切第j刀的最优位置
			while(h<t&&Kjud2(i)) --t;
			q[++t]=i;
		}
	}
	cout<<dp[n][d]<<'\n';
	int pos=n;
	Rof(i,kc,1)pos=pre[pos][i]/*返回了上次切的位置*/,cout<<pos<<' ';
	return 0;
}

T7 P

题目描述:

思路:

Code:


posted @   penggeng  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示