[POI 2005] SAM-Toy Cars

GitHub跳转原题关系图返回列表

把地板看成容量为 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;
}

复杂度

预处理下一次出现位置是 O(p)O(p)

每次请求至多做常数次 set 操作,所以总时间复杂度:

O(plogn)O(p log n)

空间复杂度:

O(n+p)O(n + p)

总结

这题的本质是:

  • 当前不用的车,不是看“现在谁最不重要”
  • 而是看“未来谁最晚再用”

一旦想到最优缓存策略,整题就是标准的下一次出现位置 + 有序集合模拟。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析