P2365 任务安排
题目描述
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
题解:我们用 分别表示时间和费用的前缀和。
先想到了一个最最最蠢的dp,表示前 i 个任务分为 j 段的最小花费,时间复杂度
,转移方程:
这个转移方程相信大家都会写,但是过不了题,就不多bb了。
因为我们最终的答案不需要计算出分成多少段,然后发现这个这个题的复杂度可以写,
表示完成前i个任务对最终答案的最小费用贡献,转移方程:
其中 表示当我们在i前面找到一个j,并且将
划分为新的一段,那么在 j 后面的每个任务都多了一个时间s产生的费用贡献。
表示不考虑s,每批任务应产生的贡献。
int t[maxn], c[maxn], sumt[maxn], sumc[maxn];
LL dp[maxn]; // 完成前i个任务,对最终答案产生的最小贡献
int main(){
int n, s; cin >> n >> s;
for(int i=1; i<=n; i++) {
scanf("%d%d", &t[i], &c[i]);
sumt[i] = sumt[i-1] + t[i];
sumc[i] = sumc[i-1] + c[i];
}
MT(dp, INF); dp[0] = 0; // INF 开大一点
for(int i=1; i<=n; i++) for(int j=0; j<i; j++)
dp[i] = min( dp[i],
dp[j] + 1ll * s * (sumc[n] - sumc[j]) + 1ll * sumt[i] * (sumc[i] - sumc[j]) );
printf("%lld\n", dp[n]);
return 0;
}
上面这种写法可以过掉这个题,但是做这个题的目的就是学习斜率优化,所以我们来看看斜率优化的复杂度怎么做的。
我们把 转换为:
。
我们就得到了一个 作为变量,
作为因变量的函数。
其斜率为 ,截距为
。
我们要使得 最小,则需要找到最小的截距。
最蠢的方法就是枚举每个决策点 (,
) ,带进去得到 j 个方程,取其中最小的截距来计算出最小的
,但是这种做法的复杂度无疑是
。
那怎么降低复杂度呢?
此时我们就可以用单调队列维护决策点 (,
)点集了,保证其中相邻点的斜率必定是递增的。
当需要做出决策的时候,我们二分这个点集,找到最优转移点,这种做法的复杂度为。
但是针对于此题,我们发现斜率 是随 i 单调递增的,所以我们在单调队列中可以把队首构成斜率小于
的点pop掉(因为它必定不是最优解,此时不是,以后也不是),此时当我们计算
选取队首的决策点最优。
统计完答案后,我们用去更新队尾元素。
因为每个决策点只会进出一次,所以最后的复杂度为。
#include<iostream>
#include<sstream>
#include<iterator>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
#include<set>
#include<vector>
#include<bitset>
#include<climits>
#include<queue>
#include<iomanip>
#include<cmath>
#include<stack>
#include<map>
#include<ctime>
#include<new>
using namespace std;
#define LL long long
#define ULL unsigned long long
#define MT(a,b) memset(a,b,sizeof(a))
const LL INF = 0x3f3f3f3f3f3f;
const int O = 1e6;
const int mod = 1e6 + 3;
const int maxn = 5005;
const double PI = acos(-1.0);
//const double E = 2.718281828459;
const long double eps = 1e-12;
int t[maxn], c[maxn], sumt[maxn], sumc[maxn];
LL dp[maxn]; // 完成前i个任务,对最终答案产生的最小贡献
int q[maxn], l = 0, r = 0;
int main(){
int n, s; cin >> n >> s;
for(int i=1; i<=n; i++) {
scanf("%d%d", &t[i], &c[i]);
sumt[i] = sumt[i-1] + t[i];
sumc[i] = sumc[i-1] + c[i];
}
dp[0] = 0; q[r ++] = 0; // 队中元素为 l ~ r-1
for(int i=1; i<=n; i++) {
while(l < r - 1){
double kl = 1.0 * (dp[q[l+1]] - dp[q[l]]) / (sumc[q[l+1]] - sumc[q[l]]);
if(kl > s + sumt[i]) break;
l ++;
}
dp[i] = dp[q[l]] + s * (sumc[n] - sumc[q[l]]) + sumt[i]* (sumc[i] - sumc[q[l]]);
while(l < r - 1) {
double kr = 1.0 * (dp[q[r-1]] - dp[q[r-2]]) / (sumc[q[r-1]] - sumc[q[r-2]]);
double ki = 1.0 * (dp[i] - dp[q[r-1]]) / (sumc[i] - sumc[q[r-1]]);
if(ki > kr) break;
r --;
}
q[r ++] = i;
}
printf("%lld\n", dp[n]);
return 0;
}