StudentRecord: 2005 FRQ 4
A step-by-step solution to the 2005 AP CSA FRQ 4 (StudentRecord), covering averaging a range of an array, detecting a non-decreasing sequence, and combining both into one grading rule in Java.
A grading scheme that rewards students whose scores keep going up is the idea behind this AP Computer Science A free-response question — it's really three small building blocks (average a range, detect a trend, then combine the two) rather than one big method.
What This FRQ Tests
- AP CSA units: Unit 6 (Array), with a good amount of Unit 4 (Iteration) mixed in
- Core skill: averaging a sub-range of an array using inclusive start/end indexes
- Secondary skill: scanning an array for a non-decreasing trend, and combining two already-written helper methods instead of re-deriving their logic
- Official category: by content this is a plain 1D
int[]array question — noArrayList, no 2D array — even though it's printed as FRQ 4 on the 2005 exam, the slot the modern exam reserves specifically for 2D arrays. The fixed FRQ-number-to-category ordering (1 = Methods/Control Structures, 2 = Classes, 3 = Array/ArrayList, 4 = 2D Array) wasn't standardized until the 2019-2020 Course and Exam Description redesign, so a 2005 question's printed number doesn't guarantee that modern category.
The Setup
StudentRecordhas one field:private int[] scores, guaranteed to have more than one element.- Three methods to write, each building on the last:
average(int first, int last)— the mean ofscores[first]throughscores[last], inclusivehasImproved()—trueif every score is greater than or equal to the one before itfinalAverage()— if the scores improved, average only the second half (scores.length / 2and up); otherwise, average everything
- The question's own table of examples:
| Student Scores | Improved? | Final Average |
|---|---|---|
| 50, 50, 20, 80, 53 | No | (50 + 50 + 20 + 80 + 53) / 5.0 = 50.6 |
| 20, 50, 50, 53, 80 | Yes | (50 + 53 + 80) / 3.0 = 61.0 |
| 20, 50, 50, 80 | Yes | (50 + 80) / 2.0 = 65.0 |
Part (a): Writing average(int first, int last)
Step-by-Step Approach
- Start a running total at
0. - Loop from
firsttolast, inclusive. - Add each
scores[i]to the total. - Divide the total by the number of scores in the range — which is
last - first + 1, not justlastorlast - first.
The Code
private double average(int first, int last)
{
int sum = 0;
for (int i = first; i <= last; i++)
{
sum += scores[i];
}
return (double) sum / (last - first + 1);
}
Why Each Piece Matters
i <= last, noti < last— the precondition explicitly says the range is "inclusive," so the element at indexlasthas to be counted.last - first + 1— this is the actual number of elements between two inclusive indexes. Forgetting the+ 1undercounts by exactly one, which throws off the average every time.(double) sum—sumandlast - first + 1are bothint, and Java's/performs integer division when both sides areints. Castingsumtodoublefirst forces the division to keep its decimal part.
Tracing the Example
Using the first row of the question's table, scores = {50, 50, 20, 80, 53}, calling average(0, 4):
- Loop adds
50 + 50 + 20 + 80 + 53 = 253 - Range size:
4 - 0 + 1 = 5 (double) 253 / 5 = 50.6
That matches the table's "No" row exactly, where the final average (over the whole array) is 50.6.
Common Mistakes to Avoid
- Dividing by
last - firstinstead oflast - first + 1— undercounts the range by one element. - Casting the wrong thing, like
(double) (sum / (last - first + 1)). The integer division already truncates before that cast ever runs, so the.0it produces is meaningless. - Looping with
i < last— this silently skips the score at indexlast, which the inclusive precondition requires to be counted.
Part (b): Writing hasImproved()
The Rule, Broken Down
- Compare every score to the one right before it.
- If any score is less than the previous one, the sequence has not improved — return
falseimmediately. - If every comparison passes (each score is greater than or equal to the previous one), return
true.
Step-by-Step Approach
- Loop starting at index
1(there's no "previous" score to compare index0against). - On each iteration, compare
scores[i]toscores[i - 1]. - If
scores[i]is smaller, the whole sequence fails — returnfalseright there. - If the loop finishes without ever returning
false, every pair passed — returntrue.
The Code
private boolean hasImproved()
{
for (int i = 1; i < scores.length; i++)
{
if (scores[i] < scores[i - 1])
{
return false;
}
}
return true;
}
Why Each Piece Matters
- Starting the loop at
i = 1, noti = 0— comparingscores[0]toscores[-1]would run off the front of the array. The first score has nothing before it to compare against, so it's simply assumed to pass. scores[i] < scores[i - 1], not<=— the rule is "greater than or equal to," so two equal consecutive scores still count as improved. Only a strict decrease breaks the streak.- Returning
falsethe moment a decrease is found — a single out-of-order pair is enough to fail the whole sequence, so there's no reason to keep checking the rest. return trueonly after the loop finishes — this is the "innocent until proven otherwise" structure: the sequence is assumed to have improved unless some pair proves otherwise.
Tracing the Example
All three rows from the question's table:
| Scores | Comparisons | Result |
|---|---|---|
| 50, 50, 20, 80, 53 | 50 >= 50 ok, then 20 < 50 fails |
false (No) |
| 20, 50, 50, 53, 80 | 50>=20, 50>=50, 53>=50, 80>=53 — all pass |
true (Yes) |
| 20, 50, 50, 80 | 50>=20, 50>=50, 80>=50 — all pass |
true (Yes) |
Every result matches the "Improved?" column in the question exactly — including that the first row fails on its very first decrease and never even checks the remaining pair.
Common Mistakes to Avoid
- Starting the loop at
i = 0.scores[i - 1]becomesscores[-1], which throws anArrayIndexOutOfBoundsException. - Using
<=in the failing condition (i.e.,if (scores[i] <= scores[i - 1])). That would incorrectly reject sequences with two equal, consecutive scores — but the rule explicitly allows "greater than or equal to." - Continuing to loop after finding a decrease, or storing a
booleanflag and returning it at the end instead of returningfalseimmediately. Both can be made correct, but the earlyreturn falseis simpler and avoids extra bookkeeping.
Part (c): Writing finalAverage()
The Rule, Broken Down
- If
hasImproved()istrue, only average scores from indexscores.length / 2through the last index. - If
hasImproved()isfalse, average the entire array, from index0through the last index. - Both branches just call
average(first, last)with different bounds — no new averaging logic is needed here.
Step-by-Step Approach
- Call
hasImproved()once to decide which branch applies. - If
true, callaverage(scores.length / 2, scores.length - 1). - If
false, callaverage(0, scores.length - 1). - Return whichever call was made.
The Code
public double finalAverage()
{
if (hasImproved())
{
return average(scores.length / 2, scores.length - 1);
}
else
{
return average(0, scores.length - 1);
}
}
Why Each Piece Matters
- Calling
hasImproved()andaverage(...)instead of rewriting their logic — the question explicitly requires calling the methods from parts (a) and (b), and doing so also means this method stays correct even if the internal details of either helper ever changed. scores.length / 2— Java's integer division here matches the problem's own definition of the cutoff exactly (scores.length/2), with no extra rounding needed.scores.length - 1as the upper bound in both branches —scores.lengthitself is always one past the last valid index, so using it directly insideaveragewould read past the end of the array.
Tracing the Example
All three rows from the question's table, now run all the way through finalAverage():
| Scores | hasImproved() |
Range averaged | Calculation | Result |
|---|---|---|---|---|
| 50, 50, 20, 80, 53 | false |
average(0, 4) |
(50+50+20+80+53) / 5.0 | 50.6 |
| 20, 50, 50, 53, 80 | true |
average(2, 4) (5/2 = 2) |
(50+53+80) / 3.0 | 61.0 |
| 20, 50, 50, 80 | true |
average(2, 3) (4/2 = 2) |
(50+80) / 2.0 | 65.0 |
All three results match the question's "Final Average" column exactly, confirming that finalAverage() correctly routes to the right range in average() based on what hasImproved() decides.
Common Mistakes to Avoid
- Passing
scores.lengthinstead ofscores.length - 1as the last index — this reads one element past the end of the array insideaverage, which throws an exception. - Using
(scores.length - 1) / 2or some other adjusted formula instead of the plainscores.length / 2the problem specifies — this shifts which scores get included in the "improved" case. - Re-implementing the averaging or improvement check inline instead of calling
average(...)andhasImproved()— besides being unnecessary duplicate work, the problem specifically requires calling those methods.
Key Takeaways
- An inclusive index range's element count is
last - first + 1— a detail that's easy to drop and always shows up as an off-by-one error. - Detecting a broken trend in a sequence is a "guilty until proven innocent... in reverse" pattern: assume it holds, scan every adjacent pair, and bail out the moment one pair breaks the rule.
- When a problem hands you working helper methods and asks you to combine them, the intended solution is almost always just choosing the right arguments to pass — not rewriting what those helpers already do.