用哈希表定位缓存块,用每组的双向链表维护 LRU 顺序,并按替换先写回再读入的顺序输出内存操作。
OJ: shumeng
题目 ID: CSP202412C
难度:普及+/提高-
标签:模拟LRU哈希表双向链表
日期: 2026-07-31 16:21
形式化题目
缓存共 0 读、1 写):
- 命中:直接在缓存中访问,写指令把该缓存行标记为脏;
- 未命中:从内存读入该块;若组已满,按 LRU 规则淘汰最久未使用的缓存行,脏行需先写回内存。
输出所有实际发生的内存操作:读入块输出 0 块号,写回脏块输出 1 块号,按发生顺序输出。
思路
核心是同时维护三件事:块到缓存行的定位、每组的 LRU 顺序、每个缓存行的脏位。
朴素做法:每组用 vector 扫描
先看直接模拟:每组用一个 vector 保存缓存行,命中时线性查找,未命中且满时淘汰尾部,命中或新插入都移到头部。
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:21
* update_at: 2026-08-17 22:39
*/
// brute.cpp:小数据暴力解,用 vector 顺序扫描每组中的缓存行来模拟 LRU。
#include <bits/stdc++.h>
using namespace std;
struct Entry {
int block; // 缓存行保存的内存块编号
bool dirty; // 是否被写改过
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, group_count, q;
cin >> n >> group_count >> q;
vector<vector<Entry> > cache(group_count); // 每组一个 vector,表头是最新使用的
for (int operation = 1; operation <= q; operation++) {
int type, block;
cin >> type >> block;
int group = (block / n) % group_count;
// 线性查找当前组是否已缓存该块
int hit = -1;
for (int i = 0; i < (int)cache[group].size(); i++) {
if (cache[group][i].block == block) {
hit = i;
break;
}
}
if (hit != -1) {
// 命中:按操作更新脏位,并移到表头
Entry entry = cache[group][hit];
if (type == 1) entry.dirty = true;
cache[group].erase(cache[group].begin() + hit);
cache[group].insert(cache[group].begin(), entry);
continue;
}
// 未命中且已满:先写回最久未使用的脏块,再淘汰
if ((int)cache[group].size() == n) {
Entry victim = cache[group].back();
if (victim.dirty) cout << "1 " << victim.block << '\n';
cache[group].pop_back();
}
cout << "0 " << block << '\n';
Entry entry;
entry.block = block;
entry.dirty = (type == 1);
cache[group].insert(cache[group].begin(), entry);
}
return 0;
}做法能正确确认操作顺序,但每次访问可能扫描整组并移动元素,只适合小数据。
主解:哈希定位 + 双向链表
把两个动作分开:
- 用哈希表
location记录“内存块 → 缓存槽位”,期望判断命中并定位; - 每组维护一条双向链表,表头是最近使用、表尾是最久未使用的缓存行。命中时把节点移到表头;未命中且已满时淘汰表尾。
缓存总容量
输出顺序
未命中时顺序很重要:若尾节点是脏的,先输出 1 old_block 写回,再输出 0 block 读入。写指令命中或读入后只把缓存行标记为脏,不会立刻再产生一次内存写。
样例推演
下表记录样例中组
| 指令 | 访问 | 结果 | 实际内存操作 | 处理后 LRU(表头在左) |
|---|---|---|---|---|
| 1 | 读 0 | 未命中 | 0 0 |
0 |
| 2 | 读 1 | 未命中 | 0 1 |
1, 0 |
| 3 | 写 2 | 未命中 | 0 2 |
2*, 1, 0 |
| 4 | 读 1 | 命中 | 无 | 1, 2*, 0 |
| 5 | 写 0 | 命中 | 无 | 0*, 1, 2* |
| 6 | 读 32 | 未命中 | 0 32 |
32, 0*, 1, 2* |
| 7 | 写 33 | 未命中 | 1 2,0 33 |
33*, 32, 0*, 1 |
| 8 | 读 34 | 未命中 | 0 34 |
34, 33*, 32, 0* |
注意第 7 条:组满后淘汰的尾节点是块 2,它曾被写入过,所以先写回 1 2 再读入 0 33。
代码
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:21
* update_at: 2026-08-17 22:39
*/
#include <bits/stdc++.h>
using namespace std;
struct CacheLine {
int block;
int previous;
int next;
bool dirty;
};
struct CacheSet {
int head;
int tail;
int used;
};
vector<CacheLine> lines;
vector<CacheSet> cache_sets;
void remove_from_lru(int group, int index) {
int previous = lines[index].previous;
int next = lines[index].next;
if (previous == -1) cache_sets[group].head = next;
else lines[previous].next = next;
if (next == -1) cache_sets[group].tail = previous;
else lines[next].previous = previous;
lines[index].previous = -1;
lines[index].next = -1;
}
void insert_to_front(int group, int index) {
int old_head = cache_sets[group].head;
lines[index].previous = -1;
lines[index].next = old_head;
if (old_head == -1) cache_sets[group].tail = index;
else lines[old_head].previous = index;
cache_sets[group].head = index;
}
void touch(int group, int index) {
if (cache_sets[group].head == index) return;
remove_from_lru(group, index);
insert_to_front(group, index);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, group_count, q;
cin >> n >> group_count >> q;
lines.resize(n * group_count);
cache_sets.resize(group_count);
for (int group = 0; group < group_count; group++) {
cache_sets[group].head = -1;
cache_sets[group].tail = -1;
cache_sets[group].used = 0;
}
unordered_map<int, int> location;
location.reserve(q * 2 + 1);
for (int operation = 1; operation <= q; operation++) {
int type, block;
cin >> type >> block;
int group = (block / n) % group_count;
unordered_map<int, int>::iterator found = location.find(block);
if (found != location.end()) {
int index = found->second;
if (type == 1) lines[index].dirty = true;
touch(group, index);
continue;
}
// 未命中时先处理被替换缓存行的写回,再从内存读入新块。
int index;
if (cache_sets[group].used < n) {
index = group * n + cache_sets[group].used;
cache_sets[group].used++;
} else {
index = cache_sets[group].tail;
if (lines[index].dirty) cout << "1 " << lines[index].block << '\n';
location.erase(lines[index].block);
remove_from_lru(group, index);
}
cout << "0 " << block << '\n';
lines[index].block = block;
lines[index].dirty = (type == 1);
location[block] = index;
insert_to_front(group, index);
}
return 0;
}复杂度
设缓存总容量
- 时间:哈希表操作期望
,每条指令只做常数次链表操作,总期望 。 - 空间:缓存行与哈希表
。
总结
缓存模拟的关键不是只判断命中,而是同时维护组映射、LRU 顺序和脏位。哈希表负责快速定位,双向链表负责
图示解析
这张流程图概括一条读写指令的处理路径:
text
访问内存块 a
|- 哈希表命中
| `- 按操作更新脏位并移动到 LRU 表头
`- 未命中
|- 组未满:使用空槽位
`- 组已满:检查 LRU 尾节点并写回脏块
`- 输出读取 a,插入表头,写操作标记为脏命中和未命中都必须更新 LRU 顺序。只有替换脏块才产生实际内存写,未命中的读入操作始终先产生实际内存读。
