把地板看成容量为 k 的缓存,缺车且地板已满时始终淘汰未来最晚再次被请求的车,再配合下一次出现位置预处理即可得到最优答案。
OJ: luogu
题目 ID: P3419
难度:普及+/提高
标签:贪心模拟堆推导
日期: 2026-06-21 00:50
题意
地板上最多同时放 k 辆玩具车。
小朋友会按顺序请求 p 次玩具车。
如果当前想玩的车不在地板上,妈妈就要把它从书架拿下来;若地板已满,还必须先拿走一辆地板上的车。
只统计“从书架拿下来”的次数,要求最少。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXP = 105;
int n, k, p;
int a[MAXP];
int answer; // 当前找到的最优答案
// brute.cpp:小数据暴力解。
// 枚举每次缺车时把地板上的哪一辆拿回书架,求最少拿下来的次数。
void dfs(int pos, vector<int> floor_cars, int cost) {
if (cost >= answer) {
return;
}
if (pos > p) {
answer = cost;
return;
}
for (int x : floor_cars) {
if (x == a[pos]) {
dfs(pos + 1, floor_cars, cost);
return;
}
}
if ((int) floor_cars.size() < k) {
// 地板还没放满,直接把当前这辆车拿下来。
floor_cars.push_back(a[pos]);
dfs(pos + 1, floor_cars, cost + 1);
return;
}
// 地板已满,暴力枚举拿走哪一辆旧车。
for (int i = 0; i < (int) floor_cars.size(); i++) {
int old = floor_cars[i];
floor_cars[i] = a[pos];
dfs(pos + 1, floor_cars, cost + 1);
floor_cars[i] = old;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k >> p;
for (int i = 1; i <= p; i++) {
cin >> a[i];
}
answer = p + 1;
vector<int> start;
dfs(1, start, 0);
cout << answer << '\n';
return 0;
}这题就是经典缓存置换问题。
当地板满了而当前想玩的车又不在地板上时,最关键的问题是:该扔掉哪辆车?
最优策略是:
- 扔掉未来最晚才会再次被请求的那辆
- 如果某辆以后再也不会被请求,就优先扔它
原因很直接:
如果一辆车 A 比另一辆车 B 更早会再次被用到,你却把 A 扔掉、保留 B,那么到 A 再次出现时,你迟早还得再拿一次,而 B 在这之前根本帮不上忙。
所以每次冲突时删“未来最晚再用”的车,一定不劣,并且可以通过交换论证证明最优。
实现时先从后往前预处理:
- 每个请求位置
i - 这辆车下一次再出现是在什么时候
然后顺序模拟请求,并用一个有序集合维护当前地板上的所有车,按“下一次出现位置”排序。
于是:
- 如果当前车已经在地板上,只更新它的下一次出现时间
- 如果不在地板上,答案加一
- 地板未满:直接放
- 地板已满:删掉集合里下一次出现时间最大的那辆
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int MAXP = 500005;
const int INF = 1000000007;
int n, k, p;
int a[MAXP]; // a[i] 表示第 i 次请求的玩具车编号
int nxt_pos[MAXP]; // nxt_pos[i] 表示这次请求的车下一次什么时候再出现
int last_pos[MAXN]; // 倒着扫时,记录每种车最近一次出现的位置
bool in_floor[MAXN]; // 当前这辆车是否在地板上
int current_next[MAXN]; // 当前在地板上的这辆车,下一次会在什么时候被请求
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k >> p;
for (int i = 1; i <= p; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
last_pos[i] = INF;
}
for (int i = p; i >= 1; i--) {
nxt_pos[i] = last_pos[a[i]];
last_pos[a[i]] = i;
}
// 按“下一次出现位置”排序,位置越大说明越晚才会再用到,越应该被淘汰。
set<pair<int, int> > st;
int used = 0; // 当前地板上放了多少辆车
int ans = 0; // 从书架拿下来的总次数
for (int i = 1; i <= p; i++) {
int x = a[i];
if (in_floor[x]) {
// 已经在地板上,只需要把“下一次出现时间”更新成新的值。
st.erase(make_pair(current_next[x], x));
current_next[x] = nxt_pos[i];
st.insert(make_pair(current_next[x], x));
continue;
}
ans++;
if (used == k) {
// 地板满了,删掉未来最晚才会再被请求的那辆车。
set<pair<int, int> >::iterator it = st.end();
--it;
int del = it->second;
st.erase(it);
in_floor[del] = false;
used--;
}
in_floor[x] = true;
current_next[x] = nxt_pos[i];
st.insert(make_pair(current_next[x], x));
used++;
}
cout << ans << '\n';
return 0;
}复杂度
预处理下一次出现位置是
每次请求至多做常数次 set 操作,所以总时间复杂度:
空间复杂度:
总结
这题的本质是:
- 当前不用的车,不是看“现在谁最不重要”
- 而是看“未来谁最晚再用”
一旦想到最优缓存策略,整题就是标准的下一次出现位置 + 有序集合模拟。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
