做题随笔:P5763

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\))。

求所有指令完成的时刻和进入过等待序列的指令数。

有几个细节:

  1. 等待队列的指令优先于输入指令执行;
  2. 占用时间是(开始时间 \(t_i\)):\([t_i,t_i+P_i)\)
  3. 最终要求的是完成时间,相当于 2 中的 \(t_i+P_i\)
  4. 时间可以从 \(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;
}

闲话

如果觉得有用,点个赞吧!(调了一上午加一中午,写了一下午,蒟蒻尽力了)

posted @ 2025-02-12 18:05  Tenil  阅读(22)  评论(0)    收藏  举报