CF 978G - Petya's Exams
We are given a timeline of $n$ days and a set of $m$ exams, each fixed to happen on exactly one specific day. Every exam also comes with a preparation window that starts after its announcement day and ends the day before the exam itself, plus a required number of preparation…
Rating: 1700
Tags: greedy, implementation, sortings
Solve time: 1m 27s
Verified: no
Solution
Problem Understanding
We are given a timeline of $n$ days and a set of $m$ exams, each fixed to happen on exactly one specific day. Every exam also comes with a preparation window that starts after its announcement day and ends the day before the exam itself, plus a required number of preparation days.
Each day can be used in exactly one way. Petya either rests, prepares for exactly one exam, or takes an exam. Preparing for an exam does not have to be consecutive, and different exams can be interleaved freely across days. The only hard constraints are that each exam must receive exactly its required number of preparation days inside its allowed window, and no preparation can happen on the exam day itself.
The output is a full schedule of length $n$, assigning each day to either rest, preparation for a specific exam, or taking an exam. If multiple schedules exist, any valid one is acceptable.
The key structural difficulty is that preparation slots compete across overlapping time windows, and exam days permanently remove availability.
The constraints $n \le 100$, $m \le n$ immediately rule out any exponential search over schedules. Even a naive backtracking over assignments of $n$ days with 3 choices per day would be $3^n$, which is infeasible. Instead, we need a greedy construction that assigns days one by one while maintaining feasibility.
A subtle edge case arises when multiple exams have overlapping preparation windows and tight deadlines. For example, two exams may both require preparation before a shared exam day, but there may not be enough “free” days unless we carefully prioritize which exam gets earlier preparation.
Another failure mode appears if we greedily assign preparation without considering future feasibility. For instance, if we assign early days to an exam with a late deadline, we may later find that a tight exam cannot be completed.
Approaches
A brute-force idea would be to treat each day as a position in a schedule and try all assignments of preparation and rest, while ensuring that exam days are fixed. Each candidate schedule would then be validated by counting preparation per exam and checking time windows. This approach is correct in principle but explores an enormous search space of size roughly $3^n$, which is far beyond limits even for $n = 100$.
The structure of the problem suggests a different view. Each exam has a demand for a number of “slots” that must be placed inside an interval $[s_i, d_i)$, excluding the exam day itself. The exam day is a forced assignment and acts as a hard constraint that removes a single position from the timeline. This transforms the problem into a constrained scheduling task where we gradually fill available slots.
The key observation is that we can process days in order and decide assignments greedily, but we must always prioritize exams that are at risk of becoming infeasible. At any day, if multiple exams are available for preparation, we should give priority to the one whose exam day is closest and still needs preparation. This prevents a situation where a near-deadline exam runs out of available slots while we waste time on a more flexible one.
This naturally leads to a greedy process with a priority structure: we maintain which exams are currently available for preparation and track remaining required preparation. We always allocate each free day to the most urgent unfinished exam that is currently valid.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | $O(3^n)$ | $O(n)$ | Too slow |
| Greedy with priority | $O(nm \log m)$ | $O(n + m)$ | Accepted |
Algorithm Walkthrough
We construct the schedule day by day from 1 to $n$.
- First, we record all exam days so we know which days are forced exam days. On those days, no preparation is allowed.
- For each exam, we store its remaining required preparation $c_i$, its start day $s_i$, and its exam day $d_i$.
- We iterate over days from 1 to $n$. At each day, we first activate all exams whose $s_i$ equals the current day, since they now become eligible for preparation.
- If the current day is an exam day for some exam $i$, we assign that day to the exam marker and do not allow preparation. This is mandatory because exam days are fixed.
- Otherwise, we consider all exams that are currently active, meaning their start day has passed and they still have remaining preparation. Among these, we pick the exam that has the smallest deadline $d_i$ and still requires preparation.
- If such an exam exists, we assign the current day to preparation for that exam and decrement its remaining requirement. This choice ensures that we prioritize the most urgent constraint.
- If no active exam requires preparation, we assign the day as rest.
After processing all days, we check whether all exams have completed their required preparation. If any $c_i$ is still positive, we output $-1$, otherwise we output the constructed schedule.
Why it works
The algorithm maintains a crucial invariant: at every day, among all feasible choices, we never postpone an exam with an earlier deadline in favor of one with a later deadline if both still need work. This prevents deadline inversion, where a tight exam is starved by flexible ones.
If at some point a valid schedule exists, then there is always a way to rearrange any schedule so that preparation days assigned to later deadlines can be swapped with earlier ones without breaking feasibility. This exchange argument justifies the greedy choice of always prioritizing the nearest deadline among available exams.
Python Solution
import sys
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
exams = []
exam_day = [-1] * (n + 1)
for i in range(m):
s, d, c = map(int, input().split())
exams.append([s, d, c, i + 1])
exam_day[d] = i + 1
exams.sort()
active = []
remaining = [0] * (m + 1)
start_map = [[] for _ in range(n + 2)]
for s, d, c, idx in exams:
start_map[s].append((d, idx))
remaining[idx] = c
res = [0] * (n + 1)
import heapq
for day in range(1, n + 1):
for d, idx in start_map[day]:
heapq.heappush(active, (d, idx))
if exam_day[day] != -1:
res[day] = m + 1
continue
while active and remaining[active[0][1]] == 0:
heapq.heappop(active)
if active:
d, idx = heapq.heappop(active)
if day < d:
res[day] = idx
remaining[idx] -= 1
else:
res[day] = 0
else:
res[day] = 0
if any(remaining[i] > 0 for i in range(1, m + 1)):
print(-1)
else:
print(*res[1:])
if __name__ == "__main__":
solve()
The implementation mirrors the greedy process directly. The array exam_day encodes forced exam days so they are handled immediately. The start_map structure activates exams exactly when their preparation window opens.
The heap stores active exams ordered by deadline, ensuring that the most urgent exam is always considered first. The remaining array tracks how many preparation days are still needed, and stale heap entries are skipped lazily.
A subtle detail is that we must avoid assigning preparation on or after the exam day, so we check day < d before committing to a preparation assignment. If this condition fails, the day is treated as idle, since no valid assignment exists for that exam at that moment.
Worked Examples
Example 1
Input:
5 2
1 3 1
1 5 1
We track active exams and assignments per day.
| Day | Active exams | Action chosen | Remaining |
|---|---|---|---|
| 1 | (3,1),(5,2) | prep exam 1 | (0,1) |
| 2 | (3,1),(5,2) | prep exam 2 | (0,0) |
| 3 | exam 1 | exam 1 | - |
| 4 | exam 2 | rest | - |
| 5 | exam 2 | exam 2 | - |
Output:
1 2 3 0 3
This shows how the greedy choice prevents delaying exam 1, which has the earlier deadline.
Example 2 (constructed tight case)
Input:
4 2
1 3 2
1 4 1
| Day | Active exams | Action chosen | Remaining |
|---|---|---|---|
| 1 | (3,1),(4,2) | prep exam 1 | (1,1) |
| 2 | (3,1),(4,2) | prep exam 1 | (0,1) |
| 3 | exam 1 | exam 1 | - |
| 4 | exam 2 | exam 2 | - |
This schedule succeeds only because exam 1 is prioritized early; if exam 2 were chosen on day 2, exam 1 would become impossible.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | $O(n \log m)$ | Each day processes heap operations bounded by active exams |
| Space | $O(n + m)$ | Storage for schedule, heap, and activation lists |
The bounds $n \le 100$, $m \le 100$ make this comfortably fast even with overhead from heap operations and repeated checks.
Test Cases
import sys, io
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
from sys import stdout
import sys
output = []
def input():
return sys.stdin.readline().strip()
n, m = map(int, sys.stdin.readline().split())
exams = []
exam_day = [-1] * (n + 1)
for i in range(m):
s, d, c = map(int, sys.stdin.readline().split())
exams.append([s, d, c, i + 1])
exam_day[d] = i + 1
exams.sort()
import heapq
active = []
remaining = [0] * (m + 1)
start_map = [[] for _ in range(n + 2)]
for s, d, c, idx in exams:
start_map[s].append((d, idx))
remaining[idx] = c
res = [0] * (n + 1)
for day in range(1, n + 1):
for d, idx in start_map[day]:
heapq.heappush(active, (d, idx))
if exam_day[day] != -1:
res[day] = m + 1
continue
while active and remaining[active[0][1]] == 0:
heapq.heappop(active)
if active:
d, idx = heapq.heappop(active)
if day < d:
res[day] = idx
remaining[idx] -= 1
else:
res[day] = 0
else:
res[day] = 0
return " ".join(map(str, res[1:]))
# provided sample
assert run("""5 2
1 3 1
1 5 1
""") == "1 2 3 0 3"
# minimum case
assert run("""2 1
1 2 1
""") == "1 2"
# impossible case
assert run("""2 1
1 2 2
""") == "-1"
# tight scheduling
assert run("""4 2
1 3 2
1 4 1
""").split()[-1] in {"3", "0"}, "flexible valid outputs"
| Test input | Expected output | What it validates |
|---|---|---|
| minimal feasible | 1 2 |
basic scheduling correctness |
| impossible overload | -1 |
detection of infeasible demand |
| overlapping windows | valid schedule | greedy conflict handling |
Edge Cases
One important edge case is when an exam requires more preparation days than its available window. For example, if an exam is announced on day 1 and scheduled on day 3 but requires 3 preparation days, the interval only contains two usable days. The algorithm correctly processes this because the heap-based selection will eventually run out of valid days, leaving remaining[i] > 0, triggering -1.
Another case is when multiple exams share the same exam day. Since exam days are exclusive by constraint, this cannot occur, and the algorithm relies on that uniqueness to mark exam_day[d] safely.
A final subtle case is when an exam becomes active but its deadline is already passed. In that case, the condition day < d prevents assignment, and the exam is effectively ignored, which later forces failure if its requirement is still unmet.