BatteryCharger: 2009 FRQ 3
A step-by-step solution to the 2009 AP CSA FRQ 3 (BatteryCharger), covering summing costs from a rate-table array with hour wraparound and finding a minimum-cost starting hour in Java.
Finding the cheapest hour to plug in an electric car is the real-world hook behind this AP Computer Science A free-response question — the actual programming challenge is summing costs out of a 24-hour rate table that wraps back around to hour 0 whenever a charge runs past midnight.
What This FRQ Tests
- AP CSA units: Unit 6 (Array) and Unit 4 (Iteration)
- Core skill: looping through a fixed-size array while the index wraps around past the end using the modulus operator (
%) - Secondary skill: calling one method repeatedly from inside another to find whichever input produces the lowest result
- Official category: this year's FRQ 3 tests a plain array (Unit 6) rather than the combined "Array/ArrayList" slot used in today's fixed FRQ-number-to-category pattern. 2009 predates the 2019–2020 CED redesign that formalized that ordering, and this particular year's exam doesn't include a 2D-array question at all — the exam's own printed "3." next to this question is what actually determines the "FRQ 3" label here, not the category.
The Setup
- The given
BatteryChargerclass has:private int[] rateTable— exactly 24 entries, one hourly cost for each hour0–23; the same table applies to every day- Every cost in the table is a positive integer
- A charge period is a block of consecutive hours that cannot be interrupted, and it can run past hour
23into hour0of the next day (or even multiple days, for a long enough charge). - You're asked to write two methods:
private int getChargingCost(int startHour, int chargeTime)— the total cost of charging forchargeTimehours starting atstartHourpublic int getChargeStartTime(int chargeTime)— whichever starting hour (0–23) produces the lowest total cost for a charge of that length
Part (a): Writing getChargingCost
Step-by-Step Approach
- Start a running total at
0. - Loop
chargeTimetimes, once for each hour of the charge. - On each iteration, figure out which actual hour (
0–23) that step of the charge falls on — this isstartHourplus how many hours have elapsed so far, wrapped back into the0–23range with% 24. - Add that hour's rate-table cost to the running total.
- After the loop, return the total.
The Code
private int getChargingCost(int startHour, int chargeTime)
{
int totalCost = 0;
for (int i = 0; i < chargeTime; i++)
{
int hour = (startHour + i) % 24;
totalCost += rateTable[hour];
}
return totalCost;
}
Why the % 24 Matters
startHour + ican easily climb past23— a 7-hour charge starting at hour22reaches hour28, which isn't a valid index into a 24-entry array.% 24(the modulus, or "remainder," operator) wraps any such value back into the valid0–23range:28 % 24is4, correctly landing on hour4of the next day.- This same expression handles charges that wrap around more than once — a 30-hour charge starting at hour
22reaches "hour 51," and51 % 24correctly resolves to hour3, two days later, without any special-casing for "how many days does this cross."
Tracing the Example
Using the sample rate table from the question:
| Start Hour | Charge Time | Hours Charged (wrapped) | Total Cost | Expected |
|---|---|---|---|---|
| 12 | 1 | 12 | 40 |
40 |
| 0 | 2 | 0, 1 | 50 + 60 = 110 |
110 |
| 22 | 7 | 22, 23, 0, 1, 2, 3, 4 | 80+60+50+60+160+60+80 = 550 |
550 |
| 22 | 30 | 22, 23, 0, 1, …, 20 (wraps twice) | one full 24-hour table (3,240) plus hours 22, 23, 0, 1, 2, 3 again (470) = 3,710 |
3,710 |
Every one of the question's four sample rows checks out exactly — including the 30-hour charge, which is really just "add up the entire rate table once, then add the first 6 hours of it again," and the % 24 wraparound produces exactly that without any extra logic.
Common Mistakes to Avoid
- Computing
startHour + iand using it directly as the array index without% 24— this throws anArrayIndexOutOfBoundsExceptionthe moment a charge crosses midnight. - Applying
% 24tochargeTimeinstead of tostartHour + i. It's the hour being looked up that needs to wrap, not the number of hours being charged —chargeTimecan legitimately be larger than 24 (as in the 30-hour example). - Off-by-one on the loop bound. The loop needs to run exactly
chargeTimetimes (ifrom0tochargeTime - 1), covering that many distinct hours — notchargeTime + 1orchargeTime - 1hours.
Part (b): Writing getChargeStartTime
The Idea
getChargingCost(from part (a), or an equivalent correct version — the problem says to assume it works regardless of what was written above) already computes the cost for one candidate starting hour.- Finding the best starting hour is then just a matter of trying every possible one (
0through23) and remembering whichever produced the lowest cost.
Step-by-Step Approach
- Set up a variable to hold the best cost found so far, and a variable to hold which hour produced it.
- Start the best-cost tracker at
Integer.MAX_VALUE, so that the very first hour checked is guaranteed to look like an improvement. - Loop over every hour from
0to23. - For each one, call
getChargingCostwith that hour and the givenchargeTime. - If that cost is lower than the best found so far, update both the best cost and the best hour.
- After checking all 24 hours, return the best hour found.
The Code
public int getChargeStartTime(int chargeTime)
{
int bestStartHour = 0;
int bestCost = Integer.MAX_VALUE;
for (int startHour = 0; startHour < 24; startHour++)
{
int cost = getChargingCost(startHour, chargeTime);
if (cost < bestCost)
{
bestCost = cost;
bestStartHour = startHour;
}
}
return bestStartHour;
}
Why Each Piece Matters
Integer.MAX_VALUEas the starting "best cost." Since every real cost is a normal, bounded positive number, the very first hour checked (startHour = 0) is guaranteed to beat this placeholder and become the initial real answer.- Looping over all 24 possible hours, not just some of them. There's no shortcut that avoids checking every starting hour — the cheapest window could start anywhere in the table, so all 24 possibilities have to be tried.
- Strict
<, not<=, when comparing costs. This means that when two different starting hours tie for the lowest cost, the method keeps the first one it found rather than overwriting it with a later tie — which the problem explicitly allows ("If there is more than one possible start time that produces the minimal cost, any of those start times can be returned"). - Reusing
getChargingCostinstead of re-deriving the cost logic here. The problem's own note ("Assume thatgetChargingCostworks as specified") signals that this method is meant to be built on top of part (a), not duplicate its logic.
Tracing the Example
For a 1-hour charge (chargeTime = 1), the loop checks rateTable[startHour] directly for every hour from 0 to 23. Hour 12 has a cost of 40, the lowest value anywhere in the table, so once the loop reaches startHour = 12, bestCost updates to 40 and stays there — no later hour ever beats it. Final result: 12, cost 40 — matching the question's table exactly.
For a 2-hour charge, the question states the minimum cost is 110, achievable starting at either hour 0 or hour 23. Because the loop checks hour 0 first and only replaces the best on a strictly lower cost, it locks in startHour = 0 immediately and never overwrites it when it later reaches hour 23's tied cost of 110 — returning 0, one of the two accepted answers.
Common Mistakes to Avoid
- Starting
bestCostat0. Every real cost is positive, so a cost of0would never look "beatable," and the loop would incorrectly returnstartHour = 0regardless of the actual rate table. - Using
<=instead of<. This would make the method always settle on the last tied starting hour instead of the first — not wrong by the problem's own rules, but a common source of confusion when comparing your output to a specific worked example. - Re-implementing the cost calculation inline instead of calling
getChargingCost. It happens to give the same answer if done correctly, but it duplicates logic that already exists and ignores the problem's explicit hint to build on part (a). - Looping over
chargeTimeinstead of over the 24 possible starting hours. The thing being searched over here is which hour to start at, not how long the charge lasts —chargeTimeis fixed for the whole method call.
Key Takeaways
- Any time an index needs to "wrap around" a fixed-size array (a clock, a rate table, a circular buffer),
% arrayLengthhandles it in one expression — no special-casing needed for how many times it wraps. - "Find the input that minimizes (or maximizes) some computed value" is a standard loop pattern: initialize a tracker to a guaranteed-losing value (
Integer.MAX_VALUEfor a minimum,Integer.MIN_VALUEor similarly small for a maximum), then loop, compare, and replace. - When a problem tells you to assume an earlier part "works as specified," that's a signal to call it rather than re-deriving its logic — both simpler to write and less likely to introduce a second, inconsistent bug.