Solution
直接模拟的各种方法可以参考其他题解,复杂度大概为 \(O(k^2)\),本篇不详讲,只做引入。蒟蒻做题时想到了,但是一看 \(k\leq 9998\),感觉有点不妥,万一有个常数直接 T 掉,于是用一天调了一个 \(O(k\log^2 k)\)(好像是,已经分析不懂了),详见下文。
题意
原题链接
有 \(N\) 个连续编号的单元,\(k\) 条指令,每条指令占用 \(M_i\) 个编号尽可能小的连续单元 \(P_i\) 时间,在 \(T_i\) 时刻执行。若 \(T_i\) 时刻无法执行则进入等待队列,队头指令一旦可以执行立刻执行(除 \(k\leq 9998\) 外,其他数据均小于 \(1 \times 10^9\))。
求所有指令完成的时刻和进入过等待序列的指令数。
有几个细节:
- 等待队列的指令优先于输入指令执行;
- 占用时间是(开始时间 \(t_i\)):\([t_i,t_i+P_i)\);
- 最终要求的是完成时间,相当于 2 中的 \(t_i+P_i\);
- 时间可以从 \(0\) 开始。
分析 & 实现
1. 直接模拟
可以发现,题目的描述都是清晰的流程,且所求正是按流程操作过程中可以得到的值,而且数据范围可接受,自然可以想到直接模拟:一个 \(\text{vector}\) 当内存条,每次取最左的可行段拆开,更新时间标记;一个 \(\text{queue}\) 作等待队列,直接按照流程操作即可。
2. 堆优化
依据 1 的大体思路,我们现在有一个存 \((l_i,r_i,st_i)\) 的 \(\text{vector}\) 当内存条(\(st_i\) 即内存释放时间),一个存 \((M_i,P_i)\) 的 \(\text{queue}\) 作等待队列,现在考虑怎么优化。
第一个问题:如何在 \(T_i\) 满足条件的同时,更低代价最小化 \(l\)
先不考虑合并问题,思考如何在 \(T_i\) 满足条件的同时,最小化连续单元左端点编号 \(l\)。由于我们需要改点,理应需要能够动态维护、随机访问。如果做双关键字排序的话,其实也可以干:众所周知,\(\text{STL set}\) 可以满足动态维护和随机访问的要求,而且其内置的二分查找可加速查询;或者手写一个二叉堆亦可满足需求。但是这比较标新立异,容易出错,蒟蒻这里用的是比较常规的分离参数。
先建一个内存条依据 \(T_i\) 排序的小根堆,筛选对应满足 \(T_i\) 条件的当前内存段,将其时间标记统一更新为 \(-\text{INF}\) 后(由于 \(T_i\) 单调递增,现在已经满足的之后一定也满足),转移到一个依据 \(l_i\) 排序的小根堆中,其堆顶就是满足 \(T_i\) 条件且 \(l\) 最小的一个。这样做相当于消去了 \(T_i\) 后继续比较,自然可以求得 \(l\) 最小段。需要使用时,取第二个堆的堆顶(假设够长),用的一段更新时间标记后丢回第一个堆,省的一段留在原堆。这样做,一段区间不会被过多次数地进行排序,从而降低了复杂度,大体 \(O(n\log n)\)。
第二个问题:如何合并可用段
一个显然的结论是:连续的两段内存,左边一段的 \(r\) 等于右边一段的 \(l\) 减一。所以直接根据这个性质在上文所述的第二个堆中合并即可。但是这样其实是不行的,问题又回到了随机访问。所以考虑把 \(\text{STL priority\_queue}\) 换成 \(\text{STL vector}\),每次新元素加入时,\(\text{sort}\) 一下,或者直接二分插入,依然能够维护单调性。
当要占用单元时,先把整个 \(\text{STL vector}\) 扫一遍,合并相邻内存段,再找到第一个长度不小于 \(M_i\) 的内存段,直接用它。这里还可以再跑一个二分查找,不过本蒟蒻就直接扫了。
第三个问题:如何解决等待队列
这个其实就比较简单了,每次先考虑等待队列中的元素即可。但是需要注意一点:等待队列中的指令执行不止可能在新指令执行前,还可能是任何内存释放时,所以说需要记录一下所有使用中的内存的释放时间,考虑开一个时间队列。由于等待队列中的指令是一有机会就执行,所以说应该用小根堆存。注意:\(\text{STL priority\_queue}\) 默认大根堆!
大方向上就是这些,其他的一些细节问题在代码里。
Code
#include <iostream>
#include <cstdio>
#include <cctype>
#include <queue>
#include <vector>
#include <algorithm>using namespace std;typedef long long ll;inline ll fr() {ll x=0,f=1;char c=getchar();while(!isdigit(c)) {if(c=='-') f=-1;c=getchar();}while(isdigit(c)) {x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}struct qus{ll l,r,st;qus (ll _l,ll _r,ll _st) {l=_l,r=_r,st=_st;}bool operator <(const qus &q1) const &{return q1.st<st;//注意:这样的小于其实返回的是大于的结果,但是对上大根堆就正好}
}; bool cmp(qus q1,qus q2) {return q1.l<q2.l;//这个才是正常的
}struct wait{ll m,p;wait (ll _m,ll _p) {m=_m,p=_p;}
};const int maxn=1e4+100;
ll n,anst,ansn;
priority_queue<qus> Q;//上文“第一个堆”
priority_queue<int,vector<int>,greater<int> > tq;//时间队列,小根堆
deque<wait> q;//等待队列
vector<qus> v;//上文“第二个堆”int main() {n=fr();Q.push(qus(1,n,-1145141919810));ll t=fr(),m=fr(),p=fr(),tt=0,mm=0,pp=0;//tt,mm,pp表示本次使用的t,m,p的值bool flag=true;while(m||!q.empty()) {//正常输入不可能占用0个单元bool wt=false;//是否使用等待队列元素if(!q.empty()&&!tq.empty()) {if(tq.top()<=t||!m) {tt=tq.top();mm=q.front().m;pp=q.front().p;q.pop_front();wt=true;}else {tt=t;mm=m;pp=p;}}else {tt=t;mm=m;pp=p;}
//上文“第一个问题”if(!Q.empty()) {while(Q.top().st<tt) {v.push_back(qus(Q.top().l,Q.top().r,-1145141919810));Q.pop();if(Q.empty()) break;}}
//上文“第二个问题”sort(v.begin(),v.end(),cmp); int lim=(int)v.size();for(register int i = 0; i < lim; i++) {if(v[i].l>v[i].r) {//非法判断v.erase(v.begin()+i);lim--;i--;//erase的时候删除点后面的都会减一,如果i++了反而跳过一个}}for(register int i = 1; i < lim; i++) {if(v[i-1].r==v[i].l-1) {v[i].l=v[i-1].l;v.erase(v.begin()+i-1);lim--;i--;}}bool vis=false;for(register int i = 0; i < (int)v.size(); i++) {qus x=v[i];if(x.r-x.l+1>=mm) {Q.push(qus(x.l,x.l+mm-1,tt+pp-1));//这里减一,所以上面“第一个问题”处不能取等v[i].l+=mm;tq.push(tt+pp);//第三个问题:时间队列赋值的时候取的等,所以这里不能减vis=true;break;}}if(!vis) {if(!wt) {q.push_back(wait(mm,pp));ansn++;}else {q.push_front(wait(mm,pp));//等待队列中的不可行,要扔回去tq.pop();}}if(!wt) {if(m) {if(!tq.empty()) {while(tq.top()<t) {tq.pop();if(tq.empty()) break;//小于当前时间的一定不行了(可行的话之前就用了),剪掉}}t=fr(),m=fr(),p=fr();}}}while(!Q.empty()) {anst=Q.top().st;Q.pop();//小根堆,最后一个最大}printf("%lld\n%lld\n",anst+1,ansn);//上面减了一,这里要加回来return 0;
}
闲话
如果觉得有用,点个赞吧!(调了一上午加一中午,写了一下午,蒟蒻尽力了)