用按长度排序的空闲区间维护 best-fit 分配器,并记录每个进程接口的循环写入位置。
OJ: shumeng
题目 ID: CSP202603C
难度:未知
标签:模拟有序集合区间合并数据结构
日期: 2026-07-31 16:22
形式化题目
把内存看作从地址
new p L:为进程新建编号递增的接口与队列,按 best-fit 原则分配一段长度为 的内存,输出分配区间的左端点 ; send p:进程向它对接的所有接口各发送一个对象,对象在各自队列内按 循环存放,输出本次所有写入地址之和; delete p i:删除进程的第 个接口,释放其内存区间,其余编号大于 的接口编号依次减一。
思路
空闲区间的两种视角
分配需要“长度最短且靠左”的区间,释放需要“按地址合并相邻区间”,两者排序依据不同,因此用两个有序结构同时维护空闲区间:
free_by_left(map<左端点, 右端点>):支持按地址找到相邻区间并合并;free_by_size(set<(长度, 左端点)>):用lower_bound直接找到长度至少为的最短区间。
text
分配: set 找 best-fit 区间 -> map 取区间 -> 左端取 L 个地址 -> 剩余放回
释放: 加入 [a,b] -> 与左右相邻空闲区间合并 -> 两个结构同步更新队列的循环写入
每个接口只需记住区间端点 [a,b] 和最近一次写入位置 last。发送时如果 last 还是初始值或已经到 b,就写回 a;否则 last+1。
删除操作
释放内存区间后调用区间合并,同时从进程的接口列表中删掉对应项即可,编号自动前移的效果由 vector::erase 天然完成。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:22
* update_at: 2026-08-17 22:40
*/
#include <bits/stdc++.h>
using namespace std;
// 内存被看作 [0, INF] 的一段连续空间,初始全部空闲。
const long long INF = 4000000000000000000LL;
// 一个进程接口对应的队列:占用的内存区间 [left, right] 与最近一次写入位置
struct QueueInfo {
long long left;
long long right;
long long last; // 最近一次发送时对象写入的地址,-1 表示尚未发送过
};
// 空闲区间按左端点排序,用于按地址合并相邻区间
map<long long, long long> free_by_left;
// 空闲区间按 (长度, 左端点) 排序,用于找 best-fit 区间
set<pair<long long, long long> > free_by_size;
// 从两个有序结构中删除左端点为 left 的空闲区间。
void erase_free(long long left) {
map<long long, long long>::iterator it = free_by_left.find(left);
long long right = it->second;
free_by_size.erase(make_pair(right - left + 1, left));
free_by_left.erase(it);
}
// 插入空闲区间 [left, right],并把它与左右相邻的区间合并。
void add_free(long long left, long long right) {
map<long long, long long>::iterator it = free_by_left.lower_bound(left);
// 先尝试与左侧相邻的区间合并
if (it != free_by_left.begin()) {
map<long long, long long>::iterator previous = it;
--previous;
if (previous->second + 1 >= left) {
left = previous->first;
right = max(right, previous->second);
erase_free(previous->first);
}
}
// 再向右吃掉所有与当前区间相邻的区间
it = free_by_left.lower_bound(left);
while (it != free_by_left.end() && it->first <= right + 1) {
right = max(right, it->second);
map<long long, long long>::iterator next = it;
++next;
erase_free(it->first);
it = next;
}
free_by_left[left] = right;
free_by_size.insert(make_pair(right - left + 1, left));
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
vector<vector<QueueInfo> > queues(n + 1); // queues[p] 保存进程 p 的接口队列
free_by_left[0] = INF; // 初始只有一个覆盖全部内存的空闲区间
free_by_size.insert(make_pair(INF + 1, 0));
for (int operation = 0; operation < q; operation++) {
string type;
cin >> type;
if (type == "new") {
int process, length;
cin >> process >> length;
// best-fit:长度不小于 L 且最短、最靠左的空闲区间
set<pair<long long, long long> >::iterator it =
free_by_size.lower_bound(make_pair((long long)length, LLONG_MIN));
long long left = it->second;
long long right = free_by_left[left];
erase_free(left);
if (left + length <= right) { // 取走 L 个地址后还有剩余,重新加回
add_free(left + length, right);
}
QueueInfo queue;
queue.left = left;
queue.right = left + length - 1;
queue.last = -1;
queues[process].push_back(queue);
cout << left << '\n';
} else if (type == "send") {
int process;
cin >> process;
long long answer = 0;
// 进程 p 的所有队列各写入一个对象,地址从 last 向右循环推进
for (int i = 0; i < (int)queues[process].size(); i++) {
QueueInfo &queue = queues[process][i];
if (queue.last == -1 || queue.last == queue.right) {
queue.last = queue.left; // 首次写入或绕回左端点
} else {
queue.last++;
}
answer += queue.last;
}
cout << answer << '\n';
} else { // delete 操作
int process, index;
cin >> process >> index;
QueueInfo queue = queues[process][index - 1];
add_free(queue.left, queue.right); // 释放整个区间并合并
queues[process].erase(queues[process].begin() + index - 1);
}
}
return 0;
}复杂度
- 时间:
new和delete每次进行常数次级有序结构操作; send需要遍历该进程的全部接口。总时间复杂度为。 - 空间:空闲区间与接口总数均为
,空间复杂度 。
总结
同时需要“按地址合并”和“按长度选择”的结构,分别维护两种排序键可以让两个操作都保持对数复杂度。同步删除/插入时注意 map 与 set 必须保持一致,否则后续操作会读到错误区间。