FRQ
› csapa
AppointmentBook: 2023 FRQ 1
A step-by-step solution to the 2023 AP CSA FRQ 1 (AppointmentBook), covering nested loops that search for a block of consecutive free minutes and reusing that search to book an appointment in Java.
Booking a slot in a teacher's schedule is the real-world scenario behind this AP Computer Science A free-response question — you first search for an open stretch of minutes within a single class period, then reuse that search across several periods to actually reserve an appointment.
What This FRQ Tests
- AP CSA units: Unit 3 (Boolean Expressions and if Statements), Unit 4 (Iteration), and Unit 5 (Writing Classes/methods)
- Core skill: using nested loops to scan a fixed range (the minutes in a period) for the first position where a run of consecutive values all satisfy a condition
- Secondary skill: building a second method on top of a first one you just wrote, instead of duplicating its logic
- Official category: "Methods and Control Structures" — always FRQ 1 on the AP CSA exam
The Setup
- Appointments happen during one of 8 class periods (numbered 1–8); each period has 60 minutes, numbered 0–59.
- Two private helper methods already exist and cannot be modified:
boolean isMinuteFree(int period, int minute)— is this single minute open?void reserveBlock(int period, int startMinute, int duration)— marks a whole block of minutes as taken
- You're asked to write two public methods:
int findFreeBlock(int period, int duration)— find the first minute where aduration-minute-long free block starts; return-1if none existsboolean makeAppointment(int startPeriod, int endPeriod, int duration)— search several periods in order, and reserve the first free block found
Part (a): Writing findFreeBlock(int period, int duration)
The Rule, Broken Down
- A "block" means
durationconsecutive minutes that are all individually free. - Blocks are checked in order of their starting minute (0, then 1, then 2, ...) — the first one found is the answer.
- The last minute a block could possibly start on is
60 - duration, since a block can't run past minute 59. - If no starting minute produces a fully-free block, the method returns
-1.
Step-by-Step Approach
- Loop over every possible starting minute, from
0up to60 - duration. - For each candidate starting minute, check every minute inside that candidate's block using
isMinuteFree. - If any single minute in the block turns out to be taken, that candidate fails — move on to the next starting minute.
- If every minute in the block is free, that starting minute is the answer — return it right away.
- If the outer loop finishes without ever returning, no valid block exists — return
-1.
The Code
public int findFreeBlock(int period, int duration)
{
for (int start = 0; start <= 60 - duration; start++)
{
boolean blockIsFree = true;
for (int minute = start; minute < start + duration; minute++)
{
if (!isMinuteFree(period, minute))
{
blockIsFree = false;
}
}
if (blockIsFree)
{
return start;
}
}
return -1;
}
Why Each Piece Matters
start <= 60 - duration— this bound stops the search before it ever tests a block that would spill past minute 59. Getting this off by one (<instead of<=, or forgetting to subtractdurationat all) either misses a valid last-minute block or lets the inner loop run off the end of the period.- The inner loop checks every minute, not just the first one. A block only counts if every minute inside it is free — checking just
isMinuteFree(period, start)would wrongly accept a block that starts free but has a taken minute somewhere in the middle. - Returning inside the outer loop, the moment a valid block is found, guarantees the earliest qualifying start minute is the one returned — exactly what the problem requires when multiple valid blocks exist.
Tracing the Example
Using the sample data from the question — period 2's minutes:
| Minutes | Available? |
|---|---|
| 0–9 (10 minutes) | No |
| 10–14 (5 minutes) | Yes |
| 15–29 (15 minutes) | No |
| 30–44 (15 minutes) | Yes |
| 45–49 (5 minutes) | No |
| 50–59 (10 minutes) | Yes |
findFreeBlock(2, 15): starting minutes 0–29 all fail (either landing on unavailable minutes, or, for starts in 10–14, running into the unavailable 15–29 stretch before 15 minutes are up). Start30succeeds — minutes 30–44 are exactly the 15 free minutes needed. Returns 30, matching the question.findFreeBlock(2, 9): the 10–14 stretch is only 5 minutes long, too short for 9. Start30is again the first success (30–38 all fall inside the free 30–44 stretch). Returns 30, matching the question — even though a 9-minute block only needs part of the 30–44 range, 30 is still the earliest valid start.findFreeBlock(2, 20): the largest free stretch anywhere in period 2 is 15 minutes (30–44). No starting minute can produce 20 consecutive free minutes. The loop finishes without returning, so it returns -1, matching the question.
Common Mistakes to Avoid
- Getting the upper bound wrong — using
start < 60 - durationskips a valid final starting minute; forgetting the- durationentirely lets the inner loop check minutes beyond 59. - Checking only the candidate's first minute instead of the whole block. A block that starts free but becomes unavailable partway through must still be rejected.
- Not stopping early once a match is found. It isn't strictly wrong to keep looping, but returning immediately is simpler and directly reflects "the first/earliest block."
Part (b): Writing makeAppointment(int startPeriod, int endPeriod, int duration)
Step-by-Step Approach
- Loop through the periods from
startPeriodtoendPeriod, inclusive. - For each period, call
findFreeBlockto search for a valid block. - If a valid block is found (the result isn't
-1), callreserveBlockwith that period, that starting minute, andduration, then returntrueimmediately. - If the loop finishes without ever finding a block in any period, return
false.
The Code
public boolean makeAppointment(int startPeriod, int endPeriod, int duration)
{
for (int period = startPeriod; period <= endPeriod; period++)
{
int start = findFreeBlock(period, duration);
if (start != -1)
{
reserveBlock(period, start, duration);
return true;
}
}
return false;
}
Why Each Piece Matters
- Reusing
findFreeBlockinstead of re-writing the block-search logic is exactly what the problem is testing — the note "assumefindFreeBlockworks as intended, regardless of what you wrote in part (a)" is a signal to trust and build on top of it. - Checking periods in increasing order and returning the moment a block is found guarantees "the lowest-numbered period" is the one used, since the first period that works is kept and nothing later is ever considered.
reserveBlockis only ever called with a real, valid starting minute — never with-1. Theif (start != -1)check has to come before the call, not after.
Tracing the Example
Using the question's sample data for periods 2, 3, and 4:
| Period | Minutes | Available? |
|---|---|---|
| 2 | 0–24 (25 minutes) | No |
| 2 | 25–29 (5 minutes) | Yes |
| 2 | 30–59 (30 minutes) | No |
| 3 | 0–14 (15 minutes) | Yes |
| 3 | 15–40 (26 minutes) | No |
| 3 | 41–59 (19 minutes) | Yes |
| 4 | 0–4 (5 minutes) | No |
| 4 | 5–29 (25 minutes) | Yes |
| 4 | 30–43 (14 minutes) | No |
| 4 | 44–59 (16 minutes) | Yes |
makeAppointment(2, 4, 22): period 2's biggest free stretch is only 5 minutes, period 3's is 19 — both too small for 22. Period 4's 5–29 stretch is 25 minutes long, enough for 22;findFreeBlock(4, 22)returns5.reserveBlock(4, 5, 22)marks minutes 5–26 unavailable. Returns true — matches the question exactly.makeAppointment(3, 4, 3): period 3 is checked first. Its 0–14 stretch easily fits 3 minutes;findFreeBlock(3, 3)returns0.reserveBlock(3, 0, 3)marks minutes 0–2 unavailable. Returns true — matches, and period 4 is never even examined.makeAppointment(2, 4, 30): by this point period 2's largest stretch is 5, period 3's is 12 (after the previous call), period 4's is 16 (after the first call) — none reach 30. The loop finishes with nothing reserved, returning false — matches.
Common Mistakes to Avoid
- Calling
reserveBlockunconditionally, even whenfindFreeBlockreturned-1— this reserves a nonsensical block starting at minute-1. - Checking every period before reserving anything, instead of stopping at the first success. This can violate "the lowest-numbered period" if a later period is accidentally preferred, and it's unnecessary extra work.
- Forgetting to
return trueimmediately after a successful reservation, or accidentally falling through toreturn falseafterward.
Key Takeaways
- Searching a fixed range for the first position where a multi-step condition holds is a nested-loop pattern: the outer loop tries each position, the inner loop verifies the condition holds across the whole span.
- Once a helper method is written and trusted, later methods should call it rather than re-implement the same search.
- Returning immediately on success both simplifies the control flow and naturally guarantees "the first/earliest" result the problem is asking for.