P10979 任务安排 2

容易发现这题就是 P2365\(O(n)\) 做法。

先考虑很裸的状态 \(dp_{i,j}\),表示当前考虑到第 \(i\) 位,共分为 \(j\) 批任务的最小代价。显然有转移

\[dp_{i,j}=\min\limits _{k=0} ^{i-1}\{dp_{k,j−1}+(s\times j+\sum \limits _{l=1} ^{i}T_l)\times \sum \limits _{l=k+1} ^{i} C_l\} \]

发现这个东西很慢,复杂度来到 \(O(n^4)\)。考虑前缀和,能够优化到 \(O(n^3)\)\(sumT_i=\sum \limits _{j=1} ^{i}T_j\)\(sumC_i=\sum \limits _{j=1} ^{i} C_j\)。下文同。


用一个叫做费用提前计算优化的东西。考虑现有状态 \(dp_{i,j}\) 中,我们拿 \(j\) 只是用来计算 \(s\times j\) 的。如果我们令从 \(l\) 转移到 \(r\) 表示 \(l+1\)\(r\) 内的任务分为一批,那么当前是第几批不重要。此时启动时间 \(s\) 总会对后面的处理增加代价,所以直接加上即可。发现状态的第二维可以被优化。

转移为

\[dp_i=\min\limits _{j=1} ^{i}\{dp_{j-1}+sumT_i\times (sumF_i−sumF_{j-1})+s\times(sumF_n−sumF_{j-1})\} \]

其中 \(s\times(sumF_n−sumF_{j-1})\) 为当前分批造成的启动时间对后面所有任务带来的代价。

此时复杂度是 \(O(n^2)\) 的。


我们无法用单调数据结构优化它,原因在于 \(i\)\(j\) 混在一起。也就是说,我们需要对它继续变形。

这里 \(\min\) 本质上在求最小的 \(dp_i\),也就是说找到一个 \(j\) 使得 \(dp_i\) 最小即可。我们把它去掉,并对原式展开,有:

\[dp_i=dp_{j-1}-sumF_{j-1}\times sumT_i-s\times sumF_{j-1}+sumT_i\times sumF_i+s\times sumF_n \]

会发现我把 \(i\)\(j\) 分离开了。

不妨记 \(b=dp_i-sumT_i\times sumF_i-s\times sumF_n\)\(x=sumF_{j-1}\)\(k=sumT_i+s\),对原式移项然后带入,变成了一个很美观的东西:

\[b=y-xk \]

它可以变成我们很喜欢的 \(y=xk+b\),也就是说这个东西就是一个一次函数。

因为 \(sumT_i\times sumF_i-s\times sumF_n\) 是定值,所以最终我们希望 \(b\) 最小。\(dp_i\) 一定需要由某个 \(j\) 转移而来,也就是说,需要有某个 \((x,y)\) 在该直线上。代入 \(x,y\)\((sumF_{j-1},dp_{j-1})\)。然后可以把它们扔到平面直角坐标系上,拿直线从下往上扫,第一个点就是可以更新的 \(j\)

所以我们将问题转化为:有 \(n\) 个点在平面直角坐标系上,有一条斜率固定的直线,问最小的截距是多少。


我们考虑维护下凸的点集,如果不知道是什么也没有关系,可以看下面这个东西:

由于技术原因,没能画出 \(x\)\(y\) 轴,就姑且把图片的左下角看作原点吧。

可以大致理解为先下降后上升的一些点(上凸反之)。当我们拿任意一条直线从下往上去“切”这些点时,只有下凸点集上的点可能被“切”到。如上图,不可能切到 \(D\) 点。

考虑如何维护。假设上图按照字母顺序加点,已经加入 \(D\) 点。这个时候所有点都在下凸点集内。此时要加入一个点 \(E\),发现 \(D\) 点不能在下凸点集中,否则不下凸。

也就是说,若 \(CD\) 斜率大于 \(DE\) 斜率,那么 \(D\) 将不会再出现在下凸点集中了。

\(C\) 的坐标为 \((C_x,C_y)\),以此类推,那么 \(D\) 将不在点集内仅当 \(\frac{D_y-C_y}{D_x-C_x}\ge \frac{E_y-D_y}{E_x-D_x}\)。事实上三点共线舍去不劣。

这个东西就可以拿单调队列维护了。答案是在单调队列中第一次出现的,斜率大于或等于当前考虑直线的斜率的线段的靠左的端点(好绕啊)。

发现直线 \(y=xk+b\) 的斜率递增,所以可以及时排除掉无法成为答案的线段。

然后把计算斜率的部分叉乘一下,防止挂精度。


inline int read(){
	char ch=getchar();
	int s=0;
	int w=1;
	while(ch<'0' or ch>'9'){
		if(ch=='-')
			w=-1;
		ch=getchar();
	}
	while(ch>='0' and ch<='9'){
		s=(s<<3)+(s<<1)+(ch xor 48);
		ch=getchar();
	}
	return s*w;
}//快读自动掠过
int n;
int s;
class node{
	public:
		int f=0;
		int t=0;
		inline friend node operator+(node a,node b){
			node c;
			c.t=a.t+b.t;
			c.f=a.f+b.f;
			return c;
		}
}a[100086],sum[100086];
class Que{
	public:
		int q[1000086];
		int l=1;
		int r=0;
		inline void pop(){
			l++;
			return;
		}
		inline void push(int x){
			q[++r]=x;
			return;
		}
		inline void pop_b(){
			r--;
			return;
		}
		inline bool empty(){
			return l>=r;
		}
}q;//手写了一下双端队列
int dp[1000086];
inline bool check(int ax,int ay,int bx,int by,int cx,int cy){
	return (by-ay)*(cx-bx)>=(cy-by)*(bx-ax);//交叉相乘
}

#define k (s+sum[i].t)
#define c (sum[i].t*sum[i].f+s*sum[n].f)
signed main(){
	n=read();
	s=read();
	memset(dp,0x7f,sizeof dp);
	for(int i=1;i<=n;i++){
		a[i].t=read();
		a[i].f=read();
		sum[i]=a[i]+sum[i-1];//维护前缀和
	}
	dp[0]=0;
    q.push(0);//一定要往里面丢一个0,否则无法更新
	for(int i=1;i<=n;i++){
		while((not q.empty()) and dp[q.q[q.l+1]]-dp[q.q[q.l]]<=k*(sum[q.q[q.l+1]].f-sum[q.q[q.l]].f))//若线段斜率小于直线的斜率,那么它将永远不会成为答案
			q.pop();
		dp[i]=dp[q.q[q.l]]-k*sum[q.q[q.l]].f+c;//此时的队首就是可更新的 j
		while((not q.empty()) and check(sum[q.q[q.r-1]].f,dp[q.q[q.r-1]],sum[q.q[q.r]].f,dp[q.q[q.r]],sum[i].f,dp[i]))//凸包更新
			q.pop_b();
		q.push(i);//不要忘记把当前点扔进去
	}
	std::cout<<dp[n];
	return 0;//撒花
}

斜率优化好可爱。

posted @ 2024-12-21 17:13  立花廿七  阅读(25)  评论(0)    收藏  举报