模拟题9——CSP202603C. 进程通信
一、总体题意题目要求我们模拟一个操作系统中的内存分配与进程通信过程。系统拥有一段初始为空的全局内存地址从 (0) 开始。虽然内存容量达到了 (10^{100})但题目保证内存一定足够使用。系统中一共有 (n) 个进程每个进程可以建立多个接口。每个接口都会对应一个队列而每个队列需要占用一段连续的内存空间。一共有三种操作new p L为进程 (p) 创建一个容量为 (L) 的新队列send p进程 (p) 向它的所有接口分别发送一个对象delete p i删除进程 (p) 的第 (i) 个接口及其对应队列。下面分别说明这三种操作。1.new p L为进程 (p) 创建一个长度为 (L) 的新队列。分配内存时采用最优适应原则找到所有连续的未占用区间只考虑长度不小于 (L) 的区间在这些区间中选择长度最短的如果有多个长度相同的区间则选择左端点最小的从选中区间的左端点开始分配连续 (L) 个地址。操作完成后需要输出新队列的起始地址。例如当前已经占用[2,4]、[9,11]那么空闲区间为[0,1]、[5,8]、[12,∞)如果要申请长度为 (3) 的队列则会选择[5,8]并分配[5,7]输出起始地址52.send p进程 (p) 会同时向它的所有接口发送一个对象。每个接口对应的队列都可以看作一个循环队列第一次发送时写入队列起始地址之后每次向后移动一个地址到达队列末尾后下一次重新回到起始地址如果某个地址原来已经存有对象新的对象可以直接覆盖它。操作完成后需要输出本次所有对象写入地址的总和。例如某个队列占用区间[9,11]连续发送五次对象写入的位置依次为9、10、11、9、10因此不需要记录每个位置具体保存了什么对象只需要记录这个队列下一次应该写入的位置。3.delete p i删除进程 (p) 的第 (i) 个接口以及对应队列。需要完成以下操作释放该队列占用的整段内存删除队列中的所有对象将编号大于 (i) 的接口编号依次减一。例如某个进程原来拥有四个接口1号、2号、3号、4号删除第 2 个接口后剩余接口编号变成1号、2号、3号其中新的 2 号接口就是原来的 3 号接口。二、思路解析1. 为什么不能直接开内存数组题目给出的内存容量为显然不能定义一个长度为的数组。不过我们并不需要保存每个内存地址的状态。观察三种操作可以发现new只关心哪些区间已经被占用delete会直接释放一个完整区间send只关心当前队列下一次写入的位置内存中对象的具体内容不会影响后续操作。因此我们只需要记录当前所有已经被占用的内存区间每个进程拥有的队列每个队列下一次发送对象时的写入地址。这样就不需要真的建立一个巨大数组。2. 队列信息的设计每个接口对应一个队列我们为每个队列保存三个信息struct Queue { long long start; long long len; long long next; };各变量含义如下start队列在内存中的起始地址len队列长度next下一次发送对象时写入的地址。例如一个队列占用[5,7]那么start 5; len 3; next 5;第一次发送后写入地址5然后令next 6;第二次发送后写入地址6然后令next 7;第三次发送后写入地址7此时需要重新回到起始地址next 5;因此队列中的写入位置会按照下面的顺序循环5 → 6 → 7 → 5 → 6 → 7 → ...3. 保存每个进程的接口每个进程可以拥有多个接口并且接口编号从 (1) 开始。我们可以使用二维vectorvectorvectorQueue interfaces(n 1);其中interfaces[p]保存进程 (p) 的所有队列。由于vector的下标从 (0) 开始因此进程的 1 号接口对应下标0进程的 2 号接口对应下标1进程的 (i) 号接口对应下标i - 1。使用vector还有一个好处删除某个元素后后面的元素会自动向前移动正好符合题目中“后续接口重新编号”的要求。4. 保存所有已占用区间我们使用map保存当前所有已经被占用的内存区间maplong long, long long occupied;其中occupied[left] right;表示内存区间[left, right]已经被某个队列占用。之所以使用map是因为map会按照键从小到大自动排序。因此所有区间都会按照左端点从小到大排列。例如occupied[2] 4; occupied[9] 11;表示当前占用区间为[2,4]、[9,11]按照顺序扫描这些占用区间就可以计算出它们之间的空闲区间。5. 处理new操作假设当前占用区间按照左端点排序后为[l1,r1]、[l2,r2]、[l3,r3]那么有限的空闲区间为[0,l1-1] [r11,l2-1] [r21,l3-1]最后还有一个无限长的空闲区间[r31,∞)我们使用变量long long previousEnd -1;表示前一个占用区间的右端点。为什么初始值是-1因为内存从地址0开始所以第一个可能的空闲地址就是previousEnd 1 0对于当前占用区间[left, right]它和上一个占用区间之间的空闲段为long long freeStart previousEnd 1; long long freeEnd left - 1;空闲区间长度为long long freeLength freeEnd - freeStart 1;如果空闲区间长度不少于申请长度L就说明这个区间可以使用。根据最优适应原则我们记录当前找到的最短合法空闲区间if (freeLength L freeLength bestLength) { bestLength freeLength; bestStart freeStart; }这里只在freeLength严格小于bestLength时更新。当两个空闲区间长度相同时不进行更新。因为我们是从左向右扫描的先找到的区间左端点一定更小符合题目要求。完整扫描代码如下long long bestStart -1; long long bestLength LLONG_MAX; long long previousEnd -1; for (const auto [left, right] : occupied) { long long freeStart previousEnd 1; long long freeEnd left - 1; if (freeStart freeEnd) { long long freeLength freeEnd - freeStart 1; if (freeLength L freeLength bestLength) { bestLength freeLength; bestStart freeStart; } } previousEnd right; }如果扫描完所有有限空闲区间后仍然有bestStart -1说明没有合适的有限空闲区间。此时直接在最后一个占用区间后面分配bestStart previousEnd 1;因为最后一个空闲区间可以一直延伸到并且题目保证内存足够大所以一定可以完成分配。得到起始地址后新队列的结束地址为long long end bestStart L - 1;然后记录占用区间occupied[bestStart] end;并将队列加入进程 (p)interfaces[p].push_back({bestStart, L, bestStart});新队列的下一次写入地址就是它的起始地址。最后输出cout bestStart \n;6. 处理send操作执行send p时需要向进程 (p) 的每一个接口发送一个对象。遍历该进程的所有队列for (Queue que : interfaces[p]) { answer que.next; que.next; if (que.next que.start que.len) { que.next que.start; } }首先answer que.next;将本次写入地址加入答案。然后que.next;将下一次写入位置向后移动。队列最后一个地址是que.start que.len - 1因此当next变成que.start que.len时说明已经越过队列末尾需要回到队列起始地址que.next que.start;最终输出本次所有对象写入地址的总和。注意答案必须使用long long。因为单个地址可能达到几十亿而一次send可能向大量接口发送对象地址总和可能超过int的范围。7. 处理delete操作执行delete p i时接口编号 (i) 对应的vector下标为int index i - 1;先得到这个队列的起始地址long long start interfaces[p][index].start;由于occupied使用队列起始地址作为键因此可以直接释放对应区间occupied.erase(start);然后从进程 (p) 的接口数组中删除该队列interfaces[p].erase(interfaces[p].begin() index);删除之后后面的元素会自动向前移动。例如原来的接口编号为1、2、3、4删除 2 号接口之后原来的 3、4 号接口会自动变成新的 2、3 号接口完全符合题目要求。8. 为什么不需要记录对象状态题目中为每个地址定义了两个状态该地址是否被队列占用该地址是否存有对象。但在实际模拟中我们并不需要显式维护这两个数组。对于我们使用占用区间occupied代替。对于后续操作并不关心一个地址之前有没有对象再次发送时可以直接覆盖删除时会释放整个队列输出只要求本次对象写入地址的总和。因此只记录队列下一次写入的位置即可。9. 复杂度分析设当前系统中一共有 (m) 个队列。对于new操作需要扫描所有已经占用的区间时间复杂度为O(m)对于send操作需要遍历该进程的所有接口。假设该进程有 (k) 个接口时间复杂度为O(k)对于delete操作从map中删除区间的复杂度为 O(log m)从vector中删除元素最坏需要 O(m)。因此总时间复杂度最坏为O(q²)本题中所以 O(q²) 的模拟可以通过。空间复杂度为O(q)因为系统中同时存在的队列数量不会超过new操作的总数。三、总结这道题的重点并不是模拟每个内存地址而是进行抽象。虽然内存容量为但真正需要维护的只有每个队列占用的连续区间每个进程当前拥有的队列每个队列下一次写入的位置。使用map维护所有已占用区间可以按照地址顺序扫描空闲段从而实现最优适应内存分配。使用vector保存每个进程的接口可以自然处理接口的建立、遍历和删除后的重新编号。每个队列只需要记录下一次写入位置就可以实现循环写入不需要保存每个内存地址是否已经存有对象。完整代码如下#include iostream #include vector #include map #include climits #include string using namespace std; // 一个接口对应一个队列 struct Queue { long long start; // 队列起始地址 long long len; // 队列长度 long long next; // 下一次发送对象时写入的地址 }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, q; cin n q; // interfaces[p] 保存进程 p 当前拥有的所有队列 vectorvectorQueue interfaces(n 1); // occupied[left] right // 表示区间 [left, right] 已经被占用 maplong long, long long occupied; while (q--) { string operation; cin operation; if (operation new) { int p; long long L; cin p L; // 当前最优空闲区间的左端点 long long bestStart -1; // 当前最优空闲区间的长度 long long bestLength LLONG_MAX; // 上一个占用区间的右端点 long long previousEnd -1; // 按左端点从小到大扫描所有占用区间 for (const auto [left, right] : occupied) { // 上一个占用区间与当前占用区间之间的空闲段 long long freeStart previousEnd 1; long long freeEnd left - 1; if (freeStart freeEnd) { long long freeLength freeEnd - freeStart 1; // 最优适应选择长度最短的合法空闲段 // 长度相同时保留先找到的也就是左端点更小的 if (freeLength L freeLength bestLength) { bestLength freeLength; bestStart freeStart; } } previousEnd right; } // 没有合适的有限空闲段 // 使用最后面的无限空闲段 if (bestStart -1) { bestStart previousEnd 1; } long long end bestStart L - 1; // 记录新的占用区间 occupied[bestStart] end; // 新队列第一次发送时写入起始地址 interfaces[p].push_back({ bestStart, L, bestStart }); cout bestStart \n; } else if (operation send) { int p; cin p; long long answer 0; // 向进程 p 的所有接口发送一个对象 for (Queue que : interfaces[p]) { // 本次对象写入的位置 answer que.next; // 更新下一次写入位置 que.next; // 到达队列末尾后回到起始位置 if (que.next que.start que.len) { que.next que.start; } } cout answer \n; } else if (operation delete) { int p, i; cin p i; // 接口编号从 1 开始 // vector 下标从 0 开始 int index i - 1; // 找到对应队列的起始地址 long long start interfaces[p][index].start; // 释放该队列占用的内存 occupied.erase(start); // 删除接口 // 后面的接口会自动向前移动并重新编号 interfaces[p].erase( interfaces[p].begin() index ); } } return 0; }转载请注明出处

相关新闻

磁性排屑机一线实操经验分享

磁性排屑机一线实操经验分享

先说大前提:磨床配磁性排屑机,核心目标就两件事 ——吸干净砂轮灰、铁粉;少堵、少卡、少掉磁、少返工。很多车间买回去用半年就废,不是设备质量差,全是选型错、用法瞎、保养踩坑。下面全部是磨床现场实打实的实操心得&…

2026/10/24 12:31:20 阅读更多 →
2026年正规SEO公司怎么选:七大避坑维度+真实案例复盘+KPI对赌合同指南|参考

2026年正规SEO公司怎么选:七大避坑维度+真实案例复盘+KPI对赌合同指南|参考

核心要点:放在行业竞争中看,2026年SEO服务市场良莠不齐,企业筛选正规SEO公司,应围绕资质审查、白帽技术验证、内容质量检查、数据透明度、合同条款、KPI对赌机制以及售后退出安排七个维度展开系统评估。进一步看,本文结…

2026/10/25 23:37:20 阅读更多 →
rust derive是什么

rust derive是什么

在 Rust 中,derive 是一个派生宏(Procedural Macro),它的核心作用是让编译器自动为自定义类型(结构体、枚举等)生成特定 trait(特征)的实现代码。简单来说,就是帮你省去大…

2026/10/25 5:34:50 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/27 17:33:32 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/27 4:06:49 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/27 7:21:53 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/27 18:20:50 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/27 6:20:47 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/27 4:06:50 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 13:55:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 7:21:50 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/27 19:19:37 阅读更多 →