这道题的正解貌似是链表,但是我用了set代替链表的功能,于是乎调了有两三天。
做本题的心路历程
思路本质
%你
大体思路
观察题面和数据范围。
需要维护的信息比较复杂,大致分两部分:
- 正在执行任务的程序
对于这部分数据,我们需要储存它们的- 结束时间
- 占用内存区间
- 正在等待队列中的程序
对于这部分数据,我们需要储存- 占用时长
- 占用区间长度
在数据范围上,总体花的时间、空间在1e9级别,不可能全部维护。
而离散化也很困难,所以考虑类似珂朵莉树的做法:将可用区间缩为一个记录了左右端点的节点,用set
(平衡树)或list
(链表)保证从左到右储存,使用时就直接对节点进行简单的编辑。
总体需要维护以下三个结构:
- 等待队列
- 正在执行的任务,用堆(具体采用
priority_queue
)维护最先结束的任务 - 可用的内存区间,我选择用平衡树(
set
)从左到右维护
完成以上基本定义后,接下来具体做法有两种
- 以请求作为分界
这是我一开始的做法,就是在读入的同时进行操作,读入完成前以每一个请求作为一个阶段,每读入一个请求就检测在那之前有多少个任务结束,又有多少个正在等待的任务已经开始;读入完成后再以每一个等待队列中的任务作为分界线……然而这样做维护起来非常困难,细节问题非常多。 - 以请求和任务的结束分界
这是我后来AC的做法。
先将所有的请求读入(其实边读边做也行?),然后每一次选择下一个请求或者执行中任务的结束进行计算,取两者先发生的一个。- 如果下一个事件是某个任务的结束
- 我们把它和同一刻结束的任务从正在执行任务的堆中弹出,每弹出一个都要释放它所占用的空间
- 检测等待队列中的队首任务是否可以马上执行
- 若可以,执行并检测下一个正在等待的任务,直到队列空掉或者队首不再可以执行
- 如果是某个请求
检测它是否可以直接执行,不行就丢进等待队列。
- 如果下一个事件是某个任务的结束
具体实现
储存结构
内存:
struct interval
{
int l,r;//左右
};
bool operator <(const interval &a,const interval &b)//从左到右
{return a.l<b.l;}
typedef set<interval>::iterator it;//for循环用
set<interval>s;
正在执行
struct task
{
int l,r,end;//[start,end]
//占用内存区间[l,r]到end
//end+1时已不再占用
};
bool operator >(const task &a,const task &b)
{return a.end>b.end;}//以结束时间排序
priority_queue<task,vector<task>,greater<task> >q;//小根堆
等待队列
struct wait
{
int size,time;//占用长度,占用时间
};
queue<wait>hang;
还有一个请求队列:
struct request
{
int st,size,time;//请求发出时间,请求内存大小,占用时间
};
queue<request>reqs;
释放一个区间
void free(interval x)//x是一个区间
{
if(s.size()==0){s.insert((interval){x.l,x.r});return;}//如果没有空地直接插入就好了
//找到它左边的节点
it l=s.lower_bound(x);
if(l!=s.begin())//待会要讲,重点
--l;
if(l->r==x.l-1)//如果和它连在一起
{
int L=l->l;
s.erase(l);
l=s.insert((interval){L,x.r}).first;//合并两者
}
else l=s.insert((interval){x.l,x.r}).first;
it r=s.upper_bound(x);//它右边的节点
if(r->l==x.r+1)
{
int L=l->l,R=r->r;
s.erase(l);
s.erase(r);
s.insert((interval){L,R});//合并
}
}
占用一个区间
void use(it x,int size)//占用x节点当中最左边size格内存
{
int L=x->l,R=x->r;
s.erase(x);
if(L+size<=R)//如果还有用剩的
s.insert((interval){L+size,R});
}
主函数
int main()
{
int n;
scanf("%d",&n);
s.insert((interval){1,n});
int st,size,time;
while(scanf("%d%d%d",&st,&size,&time),st||size||time)//读入
reqs.push((request){st,size,time});
int now,cnt=0;
it i;
next:
while(reqs.size()||hang.size()||q.size())
{
if(!q.size()/*没有在执行的任务*/||reqs.size()/*有待处理的请求*/&&reqs.front().st<q.top().end/*请求更早,同时的话先处理等待队列*/)
//处理请求
{
st=reqs.front().st;
size=reqs.front().size;
time=reqs.front().time;
for(i=s.begin();i!=s.end();i++)//找个区间把它塞进去
if(i->r-i->l+1>=size)
{
q.push((task){i->l,i->l+size-1,st+time-1});
use(i,size);
reqs.pop();
goto next;//懒得打bk了,跳到next:那一行
}
hang.push((wait){size,time});//把它挂起来,进入咕咕队列
reqs.pop();
cnt++;
}
else//回收内存并处理等待队列
{
now=q.top().end+1;
while(q.size()&&q.top().end<now)
{
free((interval){q.top().l,q.top().r});
q.pop();
}
next2:
if(hang.size())
{
for(i=s.begin();i!=s.end();i++)
if(i->r-i->l+1>=hang.front().size)
{
size=hang.front().size;
time=hang.front().time;
st=now;
q.push((task){i->l,i->l+size-1,st+time-1});
use(i,size);
hang.pop();
goto next2;//处理下一个等待
}
}
}
}
printf("%d\n%d",now,cnt);
return 0;
}
卡了我两天的细节
我在上面标了一处代码
//找到它左边的节点
it l=s.lower_bound(x);
if(l!=s.begin())//待会要讲,重点
--l;
这就是我调了三天还在电脑上下了个VirtualBox
的原因
有一种可能是这样的:s.lower_bound(x)==s.begin()
我们来看看zh.cppreference.com
是怎么说的:
GG
让我们来欣赏一下这一行带来的结果:
还有很多类似的情况,比如当一个容器为空的时候访问它的元素
比如
vector<int>v;
cout<<v[1];//未定义行为
而往往这些情况会导致莫名的RE,有些情况下虽然在调样例时就可以看出,但是也有些情况隐藏的很深……
所以以后我们在用STL时一定要注意各种边界,因为它里面的未定义行为和各种满天飞的指针实在是太多了,而且不同的编译器又会给出不同的实现,从而导致不同的、令人困扰结果……从而耽误你很多时间,在考场上面还会耽误你很多分数。
所以如果不是时间很紧就尽量少用STL吧。
重(dai)点(ma)
有多少人只是来抄代码的自己知道
#include<cstdio>
#include<set>
#include<queue>
using namespace std;
struct interval
{
int l,r;//左右
};
bool operator <(const interval &a,const interval &b)//从左到右
{return a.l<b.l;}
typedef set<interval>::iterator it;//for循环用
set<interval>s;
struct task
{
int l,r,end;//[start,end]
//占用内存区间[l,r]到end
//end+1时已不再占用
};
bool operator >(const task &a,const task &b)
{return a.end>b.end;}//以结束时间排序
priority_queue<task,vector<task>,greater<task> >q;//小根堆
struct wait
{
int size,time;//占用长度,占用时间
};
queue<wait>hang;
struct request
{
int st,size,time;//请求发出时间,请求内存大小,占用时间
};
queue<request>reqs;
void free(interval x)//x是一个区间
{
if(s.size()==0){s.insert((interval){x.l,x.r});return;}//如果没有空地直接插入就好了
//找到它左边的节点
it l=s.lower_bound(x);
if(l!=s.begin())//待会要讲,重点
--l;
if(l->r==x.l-1)//如果和它连在一起
{
int L=l->l;
s.erase(l);
l=s.insert((interval){L,x.r}).first;//合并两者
}
else l=s.insert((interval){x.l,x.r}).first;
it r=s.upper_bound(x);//它右边的节点
if(r->l==x.r+1)
{
int L=l->l,R=r->r;
s.erase(l);
s.erase(r);
s.insert((interval){L,R});//合并
}
}
void use(it x,int size)//占用x节点当中最左边size格内存
{
int L=x->l,R=x->r;
s.erase(x);
if(L+size<=R)//如果还有用剩的
s.insert((interval){L+size,R});
}
int main()
{
int n;
scanf("%d",&n);
s.insert((interval){1,n});
int st,size,time;
while(scanf("%d%d%d",&st,&size,&time),st||size||time)//读入
reqs.push((request){st,size,time});
int now,cnt=0;
it i;
next:
while(reqs.size()||hang.size()||q.size())
{
if(!q.size()/*没有在执行的任务*/||reqs.size()/*有待处理的请求*/&&reqs.front().st<q.top().end/*请求更早,同时的话先处理等待队列*/)
//处理请求
{
st=reqs.front().st;
size=reqs.front().size;
time=reqs.front().time;
for(i=s.begin();i!=s.end();i++)//找个区间把它塞进去
if(i->r-i->l+1>=size)
{
q.push((task){i->l,i->l+size-1,st+time-1});
use(i,size);
reqs.pop();
goto next;//懒得打bk了,跳到next:那一行
}
hang.push((wait){size,time});//把它挂起来,进入咕咕队列
reqs.pop();
cnt++;
}
else//回收内存并处理等待队列
{
now=q.top().end+1;
while(q.size()&&q.top().end<now)
{
free((interval){q.top().l,q.top().r});
q.pop();
}
next2:
if(hang.size())
{
for(i=s.begin();i!=s.end();i++)
if(i->r-i->l+1>=hang.front().size)
{
size=hang.front().size;
time=hang.front().time;
st=now;
q.push((task){i->l,i->l+size-1,st+time-1});
use(i,size);
hang.pop();
goto next2;//处理下一个等待
}
}
}
}
printf("%d\n%d",now,cnt);
return 0;
}
这真是我写的最认真的一篇题解了