CompSci.rocks
FRQcsapa

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 — no ArrayList, 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

  • StudentRecord has 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 of scores[first] through scores[last], inclusive
    • hasImproved()true if every score is greater than or equal to the one before it
    • finalAverage() — if the scores improved, average only the second half (scores.length / 2 and 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

  1. Start a running total at 0.
  2. Loop from first to last, inclusive.
  3. Add each scores[i] to the total.
  4. Divide the total by the number of scores in the range — which is last - first + 1, not just last or last - 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, not i < last — the precondition explicitly says the range is "inclusive," so the element at index last has to be counted.
  • last - first + 1 — this is the actual number of elements between two inclusive indexes. Forgetting the + 1 undercounts by exactly one, which throws off the average every time.
  • (double) sumsum and last - first + 1 are both int, and Java's / performs integer division when both sides are ints. Casting sum to double first 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 - first instead of last - 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 .0 it produces is meaningless.
  • Looping with i < last — this silently skips the score at index last, which the inclusive precondition requires to be counted.

Part (b): Writing hasImproved()

The Rule, Broken Down

  1. Compare every score to the one right before it.
  2. If any score is less than the previous one, the sequence has not improved — return false immediately.
  3. If every comparison passes (each score is greater than or equal to the previous one), return true.

Step-by-Step Approach

  1. Loop starting at index 1 (there's no "previous" score to compare index 0 against).
  2. On each iteration, compare scores[i] to scores[i - 1].
  3. If scores[i] is smaller, the whole sequence fails — return false right there.
  4. If the loop finishes without ever returning false, every pair passed — return true.

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, not i = 0 — comparing scores[0] to scores[-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 false the 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 true only 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] becomes scores[-1], which throws an ArrayIndexOutOfBoundsException.
  • 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 boolean flag and returning it at the end instead of returning false immediately. Both can be made correct, but the early return false is simpler and avoids extra bookkeeping.

Part (c): Writing finalAverage()

The Rule, Broken Down

  1. If hasImproved() is true, only average scores from index scores.length / 2 through the last index.
  2. If hasImproved() is false, average the entire array, from index 0 through the last index.
  3. Both branches just call average(first, last) with different bounds — no new averaging logic is needed here.

Step-by-Step Approach

  1. Call hasImproved() once to decide which branch applies.
  2. If true, call average(scores.length / 2, scores.length - 1).
  3. If false, call average(0, scores.length - 1).
  4. 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() and average(...) 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 - 1 as the upper bound in both branches — scores.length itself is always one past the last valid index, so using it directly inside average would 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.length instead of scores.length - 1 as the last index — this reads one element past the end of the array inside average, which throws an exception.
  • Using (scores.length - 1) / 2 or some other adjusted formula instead of the plain scores.length / 2 the problem specifies — this shifts which scores get included in the "improved" case.
  • Re-implementing the averaging or improvement check inline instead of calling average(...) and hasImproved() — 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.

Related FRQs