「CROI · R1」浣熊的语言

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

按日期统计每天原计划首学人数,再把特殊日顺延和复习偏移都压成批量计数转移,避免逐单词模拟。

OJ: luogu

题目 ID: P9553

难度:普及+/提高

标签:模拟计数推导

日期: 2026-06-19 02:45

题意

每个单词有一个原计划首次学习日 d_i,以及固定的 k 个复习偏移 t_j

如果某个学习或复习事件落在特殊日,就要顺延到后面第一个非特殊日。题目要求输出最后一天,以及每天新学和复习的数量。

样例过程表

这张表展示样例 3 中,特殊日如何把事件推迟:

天数 新学数量 复习数量
1 1 0
2 1 1
3 0 0
4 2 4

这里最关键的是第 3 天为特殊日,所以原本落在这一天的事件都会整体顺延到第 4 天。

思路

先看一个逐单词模拟的朴素程序:

cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k;
    cin >> n >> m >> k;

    vector<int> d(n + 1), special_days(m + 1), t(k + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> d[i];
    }

    set<int> special;
    for (int i = 1; i <= m; ++i) {
        cin >> special_days[i];
        special.insert(special_days[i]);
    }
    for (int i = 1; i <= k; ++i) {
        cin >> t[i];
    }

    map<int, long long> learn, review;
    int last_day = 0;

    for (int i = 1; i <= n; ++i) {
        int first_day = d[i];
        while (special.count(first_day)) {
            ++first_day;
        }
        ++learn[first_day];
        last_day = max(last_day, first_day);

        for (int j = 1; j <= k; ++j) {
            int day = first_day + t[j];
            while (special.count(day)) {
                ++day;
            }
            ++review[day];
            last_day = max(last_day, day);
        }
    }

    cout << last_day << '\n';
    for (int day = 1; day <= last_day; ++day) {
        cout << learn[day] << ' ' << review[day] << '\n';
    }
    return 0;
}

这个写法很好理解,但 n 可以到 10^6。继续逐个单词处理会有很多重复工作。

注意到题目给了两个很重要的限制:

  • d_i <= 1000
  • t_j <= 1000

也就是说,真正会涉及的日期范围不大,而且同一天原计划首次学习的所有单词,之后的复习安排完全相同。

因此可以按“日期桶”统计:

  • first_plan[day]:原计划在这天首次学习的人数;
  • learn[day]:真实在这天首次学习的人数;
  • review[day]:真实在这天复习的人数。

再预处理 next_day[day],表示从 day 开始往后第一个非特殊日。

做法分两步:

  1. 扫描 first_plan
    • 如果当天是特殊日,就把这一天的人数整批推到下一天;
    • 否则它们全部成为 learn[day]
  2. 对每个 learn[day] 和每个复习偏移 t_j
    • 先看原计划复习日 day + t_j
    • 再通过 next_day 找到真实复习日;
    • 把这批人数加到 review[target]

这样就把逐单词的重复操作压成了按日期的批量加法。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAX_DAY = 5000;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k;
    cin >> n >> m >> k;

    vector<long long> first_plan(MAX_DAY + 5, 0);
    int max_day = 0;
    for (int i = 1; i <= n; ++i) {
        int d;
        cin >> d;
        ++first_plan[d];
        max_day = max(max_day, d);
    }

    vector<int> special_days(m + 1), review_offset(k + 1);
    vector<int> is_special(MAX_DAY + 5, 0);
    for (int i = 1; i <= m; ++i) {
        cin >> special_days[i];
        is_special[special_days[i]] = 1;
        max_day = max(max_day, special_days[i]);
    }
    for (int i = 1; i <= k; ++i) {
        cin >> review_offset[i];
        max_day = max(max_day, review_offset[i]);
    }

    vector<long long> learn(MAX_DAY + 5, 0), review(MAX_DAY + 5, 0);

    // 先求每一天实际新学多少单词。特殊日上的任务整体顺延到下一天。
    for (int day = 1; day <= MAX_DAY - 2; ++day) {
        if (first_plan[day] == 0) {
            continue;
        }
        if (is_special[day]) {
            first_plan[day + 1] += first_plan[day];
        } else {
            learn[day] += first_plan[day];
        }
    }

    vector<int> next_day(MAX_DAY + 5, 0);
    next_day[MAX_DAY] = MAX_DAY;
    for (int day = MAX_DAY - 1; day >= 1; --day) {
        if (is_special[day]) {
            next_day[day] = next_day[day + 1];
        } else {
            next_day[day] = day;
        }
    }

    int last_day = 0;
    for (int day = 1; day <= MAX_DAY - 2; ++day) {
        if (learn[day] == 0) {
            continue;
        }
        last_day = max(last_day, day);
        for (int j = 1; j <= k; ++j) {
            int real_day = next_day[day + review_offset[j]];
            review[real_day] += learn[day];
            last_day = max(last_day, real_day);
        }
    }

    cout << last_day << '\n';
    for (int day = 1; day <= last_day; ++day) {
        cout << learn[day] << ' ' << review[day] << '\n';
    }
    return 0;
}

复杂度

设有效日期范围为 D,时间复杂度 O(Dk)O(D * k),空间复杂度 O(D)O(D)

总结

这题看上去像事件模拟,真正的突破点是把“单词”换成“日期计数桶”。相同计划日的一批单词会一起移动,这样就能避免处理一百万个独立对象。

一图流解析

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

一图流解析