按日期统计每天原计划首学人数,再把特殊日顺延和复习偏移都压成批量计数转移,避免逐单词模拟。
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 <= 1000t_j <= 1000
也就是说,真正会涉及的日期范围不大,而且同一天原计划首次学习的所有单词,之后的复习安排完全相同。
因此可以按“日期桶”统计:
first_plan[day]:原计划在这天首次学习的人数;learn[day]:真实在这天首次学习的人数;review[day]:真实在这天复习的人数。
再预处理 next_day[day],表示从 day 开始往后第一个非特殊日。
做法分两步:
- 扫描
first_plan:- 如果当天是特殊日,就把这一天的人数整批推到下一天;
- 否则它们全部成为
learn[day]。
- 对每个
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,时间复杂度
总结
这题看上去像事件模拟,真正的突破点是把“单词”换成“日期计数桶”。相同计划日的一批单词会一起移动,这样就能避免处理一百万个独立对象。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
