ybtAu 「动态规划」第5章 斜率优化 DP

这是 neatisaac 的金牌导航题解!
提示:部分代码中存在细节问题,不影响通过,但可能影响理解。

A. 【例题1】任务安排1

\(f_{i,j}\) 表示前 \(i\) 个任务分 \(j\) 段的最小费用,\(T_i\) 表示前 \(i\) 项任务的时间和,\(C_i\) 表示前 \(i\) 项任务的费用系数和,容易得出暴力转移方程:

\[\large f_{i,j}=\min_{k<i}(f_{k,j-1}+(T_i+Sj)(C_i-C_k)) \]

时间复杂度 \(O(n^3)\),无法通过。于是对它进行优化。

发现加一段对后面时间造成的影响是一定的,于是将状态的 \(j\) 这一维去掉,得:

\[\large f_i=\min_{j<i}(f_j+T_i(C_i-C_j)+S(C_n-C_j)) \]

时间复杂度 \(O(n^2)\),可以通过。

这启示我们可以把对后面的贡献加到前面去。

#include <iostream>
#define N 300005
int n,s,t[N],c[N],q[N],f[N],tl;
double X(int x) {return c[x];}
double Y(int x) {return f[x]-s*c[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
int bs(double x)
{
	if(tl==1) return 0;
	int l=1,r=tl;
	while(l<r)
	{
		int mid=l+r>>1;
		if(slope(q[mid],q[mid+1])<=x) l=mid+1;
		else r=mid;
	}
	return q[l];
}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>s;
	for(int i=1;i<=n;i++) std::cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
	tl=1;
	for(int i=1;i<=n;i++)
	{
		int p=bs(t[i]);
		f[i]=f[p]+t[i]*(c[i]-c[p])+s*(c[n]-c[p]);
		while(tl>1&&slope(q[tl-1],q[tl])>slope(q[tl-1],i)) tl--;
		q[++tl]=i;
	}
	std::cout<<f[n];
}

B. 【例题2】任务安排2

现在 \(O(n^2)\) 也过不了了。于是对它进行优化。

整理上式,得到:

\[f_i-T_iC_i-SC_n=\min_{j<i}(f_j-SC_j-T_iC_j) \]

将先前的决策点抽象为二维平面上的点,问题抽象为求斜率一定且过这些点的直线的最小截距,即:

\[\large b=min(y-kx) \]

其中:

\[\large x=C_j \\\large y=f_j-SC_j \\\large k=T_i \\\large b=f_i-T_iC_i-SC_n \]

而要想使截距最小,不需要遍历所有的点,只需要找这些点组成的一个凸壳,并找到斜率为 \(T_i\) 的直线与它的切点。

发现凸壳的斜率是单调递增的,而所找的 \(T_i\) 也是单调递增的。于是可以利用类似双指针的做法(也称单调队列)来解决这个问题。

#include <iostream>
#define N 10005
int n,s,t[N],c[N],f[N],q[N];
double X(int x) {return c[x];}
double Y(int x) {return f[x]-s*c[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
int main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>s;
	for(int i=1;i<=n;i++) std::cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
	int hd=1,tl=1;
	for(int i=1;i<=n;i++)
	{
		double k=t[i];
		while(hd<tl&&slope(q[hd],q[hd+1])<k) hd++;
		f[i]=f[q[hd]]+t[i]*(c[i]-c[q[hd]])+s*(c[n]-c[q[hd]]);
		while(hd<tl&&slope(q[tl-1],q[tl])>slope(q[tl-1],i)) tl--;
		q[++tl]=i;
	}
	std::cout<<f[n];
}

C. 【例题3】任务安排3

虽然时间为负在现实中不可能,但是题还是这么出了。

由于 \(T_i\) 不再单调递增,所以需要在凸壳上二分而非双指针单调队列来找切点。

本题需要注意细节,不要把 \(\ge\) 写成 \(>\)

#include <iostream>
#define int long long
#define N 300005
int n,s,t[N],c[N],q[N],f[N],tl;
double X(int x) {return c[x];}
double Y(int x) {return f[x]-s*c[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
int bs(double x)
{
	if(tl==1) return 0;
	int l=1,r=tl;
	while(l<r)
	{
		int mid=l+r>>1;
		if(slope(q[mid],q[mid+1])<=x) l=mid+1;
		else r=mid;
	}
	return q[l];
}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>s;
	for(int i=1;i<=n;i++) std::cin>>t[i]>>c[i],t[i]+=t[i-1],c[i]+=c[i-1];
	tl=1;
	for(int i=1;i<=n;i++)
	{
		int p=bs(t[i]);
		f[i]=f[p]+t[i]*(c[i]-c[p])+s*(c[n]-c[p]);
		while(tl>1&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i)) tl--;
		q[++tl]=i;
	}
	std::cout<<f[n];
}

D. 【例题4】猫的运输

发现一个饲养员能领走猫 \(i\),需要出发时间 \(t\ge T_i-\sum_{j=2}^{H_i} D_j\),而等待时间就是 \(t-(T_i-\sum_{j=2}^{H_i}D_j)\)

于是令 \(A_i=T_i-\sum_{j=2}^{H_i}D_j\),将所有猫按照 \(A_i\) 排序,一个饲养员领走的猫在这个序列上一定是连续的。而该饲养员出发的时间就是这个区间最右面的 \(A_i\)

\(f_{i,j}\) 表示前 \(i\) 个饲养员领走前 \(j\) 只猫所需的最小等待时间和,有:

\[\large f_{i,j}=\min_{k<j}(f_{i-1,k}+\sum_{x=k+1}^{j}(A_j-A_k)) \\\large =\min_{k<j}(f_{i-1,k}+A_j(j-k)-(S_j-S_k)) \\\large f_{i,j}-A_jj+S_j=\min_{k<j}(f_{i-1,k}+S_k-A_jk) \]

其中 \(S_i=\sum_{j\le i}A_j\)

变成了斜率优化的标准形式,斜率为 \(A_j\)

由于 \(A_j\) 是单调递增的,且凸壳斜率单调递增,所以可以用单调队列做法。

#include <iostream>
#include <cstring>
#include <algorithm>
#define N 100005
#define int long long
int n,m,p,d[N],a[N],f[105][N],s[N],i,q[N];
double X(int x) {return x;}
double Y(int x) {return f[i-1][x]+s[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	memset(f,0x3f,sizeof f);
	std::cin>>n>>m>>p;
	for(i=2;i<=n;i++) std::cin>>d[i],d[i]+=d[i-1];
	for(i=1;i<=m;i++)
	{
		int h,t;
		std::cin>>h>>t;
		a[i]=t-d[h];
	}
	std::sort(a+1,a+m+1);
	for(i=1;i<=m;i++) s[i]=s[i-1]+a[i];
	f[0][0]=0;
	for(i=1;i<=p;i++)
	{
		int hd=1,tl=1;
		q[1]=0;
		for(int j=1;j<=m;j++)
		{
			double k=a[j];
			while(hd<tl&slope(q[hd],q[hd+1])<k) hd++;
			int o=q[hd];
			f[i][j]=std::min(f[i-1][j],f[i-1][o]+a[j]*(j-o)-(s[j]-s[o]));
			while(hd<tl&&slope(q[tl-1],q[tl])>slope(q[tl-1],j)) tl--;
			q[++tl]=j;
		}
	}
	std::cout<<f[p][m];
}

E. 特别行动队

板子。

\(f_i\) 表示前 \(i\) 个队员的最大修正战力值,\(X_i\) 表示前 \(i\) 个人初始战力值之和,有:

\[\large f_i=\max_{j<i}(f_j+a(X_i-X_j)^2+b(X_i-X_j)+c) \\\large f_i-aX_i^2-bX_i-c=\max_{j<i}(f_j+aX_j^2-bX_j-2aX_iX_j) \]

变成了斜率优化标准形式,斜率为 \(2aX_i\)

与前几题不同,这里维护的是最大值,所以凸壳上斜率是单调递减的,而由于 \(a<0\),所以询问的斜率也是单调递减的,可以使用单调队列来做。

#include <iostream>
#define int long long
#define N 1000005
int n,a,b,c,w[N],f[N],q[N];
double X(int x) {return w[x];}
double Y(int x) {return f[x]+a*w[x]*w[x]-b*w[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n>>a>>b>>c;
	for(int i=1;i<=n;i++) std::cin>>w[i],w[i]+=w[i-1];
	int hd=1,tl=1;
	for(int i=1;i<=n;i++)
	{
		double k=2*a*w[i];
		while(hd<tl&&slope(q[hd],q[hd+1])>k) hd++;
		f[i]=f[q[hd]]+a*(w[i]-w[q[hd]])*(w[i]-w[q[hd]])+b*(w[i]-w[q[hd]])+c;
		while(hd<=tl&&slope(q[tl-1],q[tl])<slope(q[tl-1],i)) tl--;
		q[++tl]=i;
	}
	std::cout<<f[n];
}

F. 打印文章

板子。

\(f_i\) 表示前 \(i\) 个单词的最少花费,\(C_i\) 表示前 \(i\) 个单词的费用和。于是有:

\[\large f_i=\min_{j<i}(f_j+(C_i-C_j)^2+M) \\\large f_i-M-C_i^2=\min_{j<i}(f_j+C_j^2-2C_iC_j) \]

变成了斜率优化的标准形式,斜率为 \(2C_i\)

\(2C_i\) 单调递增,且凸壳斜率单调递增,使用单调队列。

注意细节,单词长度可能为 \(0\)

有多测。

#include <iostream>
#define int long long
#define N 500005
int n,m,a[N],q[N],f[N];
double X(int x) {return a[x];}
double Y(int x) {return f[x]+a[x]*a[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	while(std::cin>>n>>m)
	{
		a[0]=f[0]=0;
		for(int i=1;i<=n;i++) std::cin>>a[i],a[i]+=a[i-1],f[i]=0;
		int hd=1,tl=1;
		q[1]=0;
		for(int i=1;i<=n;i++)
		{
			double k=2*a[i];
			while(hd<tl&&slope(q[hd],q[hd+1])<k) hd++;
			f[i]=f[q[hd]]+(a[i]-a[q[hd]])*(a[i]-a[q[hd]])+m;
			while(hd<tl&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i)) tl--;
			q[++tl]=i;
		}
		std::cout<<f[n]<<'\n';
	}
}

G. 锯木厂选址

注意到我们只需要建两个锯木厂。

\(f_i\) 表示在 \(i\) 建第二个锯木厂的最小运输费用,\(D_i\) 表示第 \(i\) 棵树到山脚的距离,\(W_i\) 表示前 \(i\) 棵树的重量和,\(sum\) 表示所有木材运到山脚的运输费用。枚举第一个锯木厂的位置,运用容斥的思想,可得:

\[\large f_i=\min_{j<i}(sum-D_jW_j-D_i(W_i-W_j)) \]

解释一下,\(D_jW_j\) 表示前 \(j\) 棵树从 \(j\) 运到山脚的费用,\(D_i(W_i-W_j)\) 表示第 \(j+1\) 棵树到第 \(i\) 棵树从 \(i\) 运到山脚的费用。

展开得到:

\[\large sum-f_i-D_iW_i=\max_{j<i}(D_jW_j-D_iW_j) \]

变成了斜率优化的标准形式,斜率为 \(D_i\),单调递增,且维护的是最大值,所以可以用单调队列实现。

#include <iostream>
#define int long long
#define N 20005
int n,w[N],d[N],sum,q[N];
double X(int x) {return w[x];}
double Y(int x) {return -d[x]*w[x];}
double slope(int x,int y) {return (Y(y)-Y(x))/(X(y)-X(x));}
signed main()
{
	std::ios::sync_with_stdio(0);
	std::cin.tie(0),std::cout.tie(0);
	std::cin>>n;
	for(int i=1;i<=n;i++) std::cin>>w[i]>>d[i];
	for(int i=n;i>=1;i--) d[i]+=d[i+1],sum+=w[i]*d[i];
	for(int i=1;i<=n;i++) w[i]+=w[i-1];
	int hd=1,tl=0,ans=0;
	for(int i=1;i<=n;i++)
	{
		double k=-d[i];
		while(hd<tl&&slope(q[hd],q[hd+1])<k) hd++;
		if(hd<=tl) ans=std::max(ans,d[q[hd]]*w[q[hd]]+d[i]*(w[i]-w[q[hd]]));
		while(hd<tl&&slope(q[tl-1],q[tl])>=slope(q[tl-1],i)) tl--;
		q[++tl]=i;
	}
	std::cout<<sum-ans;
}

\[\Huge End \]

posted @ 2025-05-08 12:57  整齐的艾萨克  阅读(22)  评论(0)    收藏  举报