斜率优化DP

总述

维护队列中相邻两个元素的某种“比值”的“单调性”

因为该比值对应坐标系中的斜率

所以称为斜率优化

英文称为\(convex\space hull\space trick\)(直译:凸壳优化策略)

[例1]「TYVJ1098」任务安排 1

N个任务排成一个序列在一台机器上等待完成(顺序不得改变),这N个任务被分成若干批,每批包含相邻的若干任务。从时刻0开始,这些任务被分批加工,第i个任务单独完成所需的时间是Ti。在每批任务开始前,机器需要启动时间S,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。每个任务的费用是它的完成时刻乘以一个费用系数Fi。请确定一个分组方案,使得总费用最小。 例如:S=1;T={1,3,4,2,1};F={3,2,3,3,4}。如果分组方案是{1,2}、{3}、{4,5},则完成时间分别为{5,5,10,14,14},费用C={15,10,30,42,56},总费用就是153。

输入

第一行是N(1< =N< =5000)。 第二行是S(0< =S< =50)。 下面N行每行有一对数,分别为Ti和Fi,均为不大于100的正整数,表示第i个任务单独完成所需的时间是Ti及其费用系数Fi。

输出

一个数,最小的总费用。

样例

样例输入1

5
1
1 3
3 2
4 3
2 3
1 4

样例输出1

153

solution

状态:\(f[i][j]\),前 \(i\) 个任务分成 \(j\) 批执行的最小费用

决策:考虑第 \(j\) 批执行包含的任务

状态转移方程:

  • \(𝑠𝑢𝑚𝑇[𝑖] =\sum^i_{𝑗=1}𝑇[𝑗]\)

  • \(𝑠𝑢𝑚C[𝑖] =\sum^i_{𝑗=1}C[𝑗]\)

  • \(𝑓[𝑖][𝑗] =𝑚𝑖𝑛_{0≤k≤i}(𝑓[𝑘][𝑗 − 1] + (𝑆 ∗ 𝑗 + 𝑠𝑢𝑚𝑇[𝑖]) ∗ (𝑠𝑢𝑚𝐶[𝑖] − 𝑠𝑢𝑚𝐶[𝑘]))\)

考虑第 \(j\) 批执行的是 \((k+1)\)~\(i\) 个任务

枚举第 \(j-1\) 批和第 \(j\) 批的分界点 \(k\) 为DP的决策

时间复杂度为\(O(N^3)\)

状态能否优化?

题目并没有规定分成多少批次。

之所以需要批次,是因为想知道有多少次启动时间\(S\),从而计算出每批任务完成的时间

实际上,可以将每批任务花费的启动时间\(S\),对之后任务的影响提前计算

状态:

  • \(f[i]\),表示前 \(i\) 个任务划分成若干批执行的最小费用。

考虑当前批次执行的任务,状态转移方程:

  • \(𝑓[𝑖] =𝑚𝑖𝑛_{0≤j<i} ( 𝑓[j] +sumT[i]∗(sumC[i]-sumC[j])+𝑆 ∗ (𝑠𝑢𝑚𝐶[N] − 𝑠𝑢𝑚𝐶[j]))\)

当前批次执行的任务为第 \(j+1\)~\(i\) 个任务

机器的启动时间会对第 \(j\) 个任务以后的所有任务产生影响,提前将影响累加到最小费用中

这就是“费用提前计算”的经典思想

时间复杂度为\(O(N^2)\)

AC code

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+2;
int n,s;
int t[N],c[N],st[N],sc[N],f[N];
int main(){
	scanf("%d",&n);
	scanf("%d",&s);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&t[i],&c[i]);
		st[i]=st[i-1]+t[i];
		sc[i]=sc[i-1]+c[i];
	} 
	memset(f,0x3f,sizeof(f));
	f[0]=0;
	for(int i=1;i<=n;i++){
		for(int j=0;j<i;j++){
			f[i]=min(f[i],f[j]+st[i]*(sc[i]-sc[j])+s*(sc[n]-sc[j]));
		}
	}
	printf("%d",f[n]);
	return 0;
}

[例2]「POJ1180/IOI2002」任务安排 2

N个任务排成一个序列在一台机器上等待完成(顺序不得改变),这N个任务被分成若干批,每批包含相邻的若干任务。从时刻0开始,这些任务被分批加工,第i个任务单独完成所需的时间是Ti。在每批任务开始前,机器需要启动时间S,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。每个任务的费用是它的完成时刻乘以一个费用系数Fi。请确定一个分组方案,使得总费用最小。
例如:S=1;T={1,3,4,2,1};F={3,2,3,3,4}。如果分组方案是{1,2}、{3}、{4,5},则完成时间分别为{5,5,10,14,14},费用C={15,10,30,42,56},总费用就是153。

输入

第一行是N(1<=N<=300000)。
第二行是S(0<=S<=512)。
下面N行每行有一对数,分别为Ti和Fi,均为不大于100的正整数,表示第i个任务单独完成所需的时间是Ti及其费用系数Fi。

输出

一个数,最小的总费用。

样例

样例输入1

5
1
1 3
3 2
4 3
2 3
1 4

样例输出1

153

solution

\(𝑓[𝑖] =𝑚𝑖𝑛_{0≤j<i} ( 𝑓[j] +sumT[i]∗(sumC[i]-sumC[j])+𝑆 ∗ (𝑠𝑢𝑚𝐶[N] − 𝑠𝑢𝑚𝐶[j]))\)

  • 同时和变量 \(i\) 和变量 \(j\) 有关,单调队列不适用

\(min\)函数去掉,把关于 \(j\) 的值 \(f[j]\)\(sumC[j]\) 看作变量,其余部分看作常数。

移项得到:

  • \(𝑓[𝑗] = (𝑠𝑢𝑚𝑇[𝑖] + 𝑆) ∗ 𝑠𝑢𝑚𝐶[𝑗] + 𝑓[𝑖] − 𝑠𝑢𝑚𝑇[𝑖] ∗ 𝑠𝑢𝑚𝐶[𝑖] − 𝑆 ∗ 𝑠𝑢𝑚𝐶[𝑁]\)

\(sumC[j]\) 看作是横坐标,\(f[j]\)看作是纵坐标,就是一个形如 \(y=ax+b\)的一条直线。

斜率:\(𝑠𝑢𝑚𝑇[𝑖] + S\)

截距:\(𝑓[𝑖] − 𝑠𝑢𝑚𝑇[𝑖] ∗ 𝑠𝑢𝑚𝐶[𝑖] − 𝑆 ∗ 𝑠𝑢𝑚𝐶[𝑁]\)

对于每一个决策 j,都对应直角坐标系中的一个点 (sumC[j],f[j])

当变量 \(i\) 不变时,直线的斜率是固定值\(𝑠𝑢𝑚𝑇[𝑖] + 𝑆\)

\(sumT[i]\)\(sumC[i]\)\(S\)\(sumC[N]\)为固定值。当截距取得最小值时,f[i]也取到最小值。

求解最小截距的方法:

  • 将斜率为\(𝑠𝑢𝑚𝑇[𝑖] + 𝑆\) 的直线经过每一个决策点,截距最小的为最优决策。

  • 将直线从下往上移动,遇到的第一个决策点就是最优决策。

所有的决策点\((sum[j],f[j])\)都是有用的吗?

利用“及时排除无用决策”思想,将无用的决策点删除。

假设存在三个决策 \(j1\)\(j2\)\(j3\),对应的决策点为\((sumC[j1],f[j1])\)\((sumC[j2],f[j2])\)\((sumC[j3],f[j3])\),设三点分别为\(A\)\(B\)\(C\)

不妨设 \(j1<j2<j3\),因为T,C均为整数,有\(sumC[j_1]<sumC[j_2]<sumC[j_3]\)

有两种情况:

  • 对于上凸,无论斜率是多少,j2都不可能是最优决策,可以排除。

  • 对于下凸,j2可能是最优决策,当且仅当:
    \(\frac{f[j_2]-f[j_1]}{sumC[j_2]-sumC[j_1]}<\frac{f[j_3]-f[j_2]}{sumC[j_3]-sumC[j_2]}\)

将不可能的最优决策排除后,将剩下的点集相邻两点连线

形成的线段的斜率从左到右单调递增

需要维下凸壳

使用单调队列维护

对于斜率为k的直线,若某个点左侧线段的斜率小于k,右侧线段的斜率大于k,那么该点就是最优决策点

因为\(sumC\)是单调递增的,新的决策点的横坐标一定大于之前所有决策点的横坐标

斜率\(S+sumT[i]\)也是单调递增的

只维护相邻两点线段斜率大于\(S+sumT[i]\)的决策点,那么最优决策点就是队头

时间复杂度为\(O(N)\)

AC code

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=300005;
ll n,s,t[N],c[N];
ll st[N],sc[N],dp[N], hd=1,tl=0,q[N];
double xl(ll i,ll j) {
	return ((double)dp[j]-dp[i])/((double)sc[j]-sc[i]);
}
int main() {
	scanf("%lld",&n);
	scanf("%lld",&s);
	for(ll i=1;i<=n;i++){
		scanf("%lld%lld",&t[i],&c[i]);
		st[i]=st[i-1]+t[i];
		sc[i]=sc[i-1]+c[i];
	} 
	q[++tl]=0;
	dp[0]=0;	
	for(ll i=1; i<=n; i++) {
		while(hd<tl&&xl(q[hd],q[hd+1])<=s+st[i]) hd++;
		dp[i]=dp[q[hd]]-(s+st[i])*sc[q[hd]]+st[i]*sc[i]+s*sc[n];
		while(hd<tl&&xl(q[tl-1],q[tl])>xl(q[tl],i))	tl--;
		q[++tl]=i;
	}
	printf("%lld",dp[n]);
	return 0;
}

完结撒花❀

★,°:.☆( ̄▽ ̄)/$:.°★

posted @ 2022-07-22 19:15  _Youngxy  阅读(102)  评论(0)    收藏  举报