P2365 任务安排 (斜率优化dp)

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

题解:我们用sumt[ ], sumc[ ] 分别表示时间和费用的前缀和。

先想到了一个最最最蠢的dp,dp[ i ] [ j ]表示前 i 个任务分为 j 段的最小花费,时间复杂度O (n^3),转移方程:

dp[i][j] = min \left \{ dp[k][j-1] + (sumt[i] + j * s ) * (sumc[i] - sumc[k]) \right \}

这个转移方程相信大家都会写,但是过不了题,就不多bb了。

 

因为我们最终的答案不需要计算出分成多少段,然后发现这个这个题O(n^2)的复杂度可以写,dp[i]表示完成前i个任务对最终答案的最小费用贡献,转移方程:

dp[i] = min\left \{ dp[j] + s * (sumc[n] - sumc[j]) + sumt[i] * (sumc[i] - sumc[j]) \right \}

其中s * (sumc[n] - sumc[j]) 表示当我们在i前面找到一个j,并且将 [j+1, i]划分为新的一段,那么在 j 后面的每个任务都多了一个时间s产生的费用贡献。sumt[i] * (sumc[i] - sumc[j])表示不考虑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;
}

上面这种写法可以过掉这个题,但是做这个题的目的就是学习斜率优化,所以我们来看看斜率优化O(n)的复杂度怎么做的。

我们把   dp[i] = dp[j] + s * (sumc[n] - sumc[j]) + sumt[i] * (sumc[i] - sumc[j])   转换为:

dp[j] = (s + sumt[i]) * sumc[j] + dp[i] - s * sumc[n] - sumc[i] * sumt[i] 。

我们就得到了一个 sumc[j] 作为变量,dp[j] 作为因变量的函数。

其斜率为 (s + sumt[i]) ,截距为dp[i] - s * sumc[n] - sumc[i] * sumt[i] 。

我们要使得 dp[i] 最小,则需要找到最小的截距。

最蠢的方法就是枚举每个决策点 (sumc[j], dp[j]) ,带进去得到 j 个方程,取其中最小的截距来计算出最小的dp[i],但是这种做法的复杂度无疑是O(n ^ 2)

那怎么降低复杂度呢?

此时我们就可以用单调队列维护决策点 (sumc[j], dp[j])点集了,保证其中相邻点的斜率必定是递增的。

当需要做出决策的时候,我们二分这个点集,找到最优转移点,这种做法的复杂度为O(n log_{2}n)

但是针对于此题,我们发现斜率 s+sumt[i] 是随 i 单调递增的,所以我们在单调队列中可以把队首构成斜率小于s+sumt[i]的点pop掉(因为它必定不是最优解,此时不是,以后也不是),此时当我们计算dp[i]选取队首的决策点最优。

统计完答案后,我们用(dp[i], sumc[i])去更新队尾元素。

因为每个决策点只会进出一次,所以最后的复杂度为O(n)

#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;
}

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值