尼克的任务
题目描述
尼克每天上班之前都连接上英特网,接收他的上司发来的邮件,这些邮件包含了尼克主管的部门当天要完成的全部任务,每个任务由一个开始时刻与一个持续时间构成。
尼克的一个工作日为 n n n 分钟,从第 1 1 1 分钟开始到第 n n n 分钟结束。当尼克到达单位后他就开始干活,公司一共有 k k k 个任务需要完成。如果在同一时刻有多个任务需要完成,尼克可以任选其中的一个来做,而其余的则由他的同事完成,反之如果只有一个任务,则该任务必需由尼克去完成,假如某些任务开始时刻尼克正在工作,则这些任务也由尼克的同事完成。如果某任务于第 p p p 分钟开始,持续时间为 t t t 分钟,则该任务将在第 ( p + t − 1 ) (p+t-1) (p+t−1) 分钟结束。
写一个程序计算尼克应该如何选取任务,才能获得最大的空暇时间。
输入格式
输入数据第一行含两个用空格隔开的整数 n n n 和 k k k。
接下来共有 k k k 行,每一行有两个用空格隔开的整数 p p p 和 t t t,表示该任务从第 p p p 分钟开始,持续时间为 t t t 分钟。
输出格式
输出文件仅一行,包含一个整数,表示尼克可能获得的最大空暇时间。
样例 #1
样例输入 #1
15 6
1 2
1 6
4 11
8 5
8 1
11 5
样例输出 #1
4
提示
数据规模与约定
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 1 0 4 , 1 ≤ k ≤ 1 0 4 , 1 ≤ p ≤ n , 1 ≤ p + t − 1 ≤ n 1 \leq n \leq 10^4,1 \leq k \leq 10^4,1 \leq p \leq n,1 \leq p+t-1 \leq n 1≤n≤104,1≤k≤104,1≤p≤n,1≤p+t−1≤n。
思路
-
首先,本题的实质是区间内放不相交的线段,使空余部分尽可能少。并且能放的线段必须放。
-
其次,我们先看数据范围,这道题最多只能支持 O ( n 2 ) O(n^2) O(n2) 的时间复杂度,我一开始想到的是贪心,但后面想着想着好像能找到反例(就是你正序的来,局部依次选长度最短的任务(也就是持续时间相较于同一时间来说),就是这样会错,为什么呢?因为有的区间可能比较长,但它结束比较早,也有的区间虽然短,但结束时间晚),而且贪心只能求放线段的数量(如 凌乱的yyy/线段覆盖 ),无法求长短。
-
那么贪心失效,我们只能用dp来做了,状态表示: f i f_i fi 表示所有以 i i i结尾(这边是从末尾开始)的空闲长度最长的集合。
-
状态转移:当本时刻无任务, f i = f i + 1 + 1 f_i=f_{i+1}+1 fi=fi+1+1。
-
当本时刻有任务, f i = m a x ( f i , f i + w s u m ) f_i=max(f_i,f_{i+w_{sum}}) fi=max(fi,fi+wsum),其中 w s u m w_{sum} wsum 指的是这个时刻的任务持续的时间,为什么是加上呢?因为区间是往右的。这个的目的也就是找出选择哪一个本时刻任务使空闲时间最大化。
-
这边还有个细节就是为什么逆序遍历,参考。省流:讲的就是如果是顺序遍历的话,其价值不一定为真实可达的价值,因此初始化负无穷判断能否为真。逆序遍历的话,对于目前状态的价值一定为真,其余状态的真假会在以后判断,因此无需初始化。
AC 代码
//f[i]表示1~i的最大空闲时间
#include<iostream>
#include<algorithm>
#define int long long
using namespace std;
const int N = 1e4+10;
struct E{
int l,sum;
}e[N];
int n,k;
int f[N];
int s[N];
int cnt=1;
signed main(){
cin>>n>>k;
for(int i=1;i<=k;i++){
int a,b;
cin>>a>>b;
e[i]={a,b};
s[a]++;//同一时间多个任务选简单的
}
sort(e+1,e+1+k,[&](E a,E b){
return a.l>b.l;
});
for(int i=n;i>=1;i--){
//没有任务的时候
if(!s[i]){
f[i]=f[i+1]+1;
}else{
//有任务
for(int j=1;j<=s[i];j++){
f[i]=max(f[i],f[i+e[cnt].sum]);
cnt++;
}
}
}
cout<<f[1]<<endl;
return 0;
}