CompSci.rocks
FRQcsapa

SkyView: 2013 FRQ 4

A step-by-step solution to the 2013 AP CSA FRQ 4 (SkyView), covering reconstructing a 2D array from alternating-direction scan data and averaging a rectangular section of it in Java.

Turning a telescope's raw, back-and-forth scan data into a properly oriented picture of the sky is the challenge behind this AP Computer Science A free-response question — half the work is realizing that only every other row of the result actually needs to be reversed.

What This FRQ Tests

  • AP CSA units: Unit 8 (2D Array) and Unit 4 (Iteration)
  • Core skill: mapping data from a 1D array into a 2D array using a non-trivial ordering rule — the scan direction alternates every row
  • Secondary skill: computing an average over an arbitrary rectangular sub-section of a 2D array with nested loops
  • Official category: this is squarely "2D Array" content, the same category that's always FRQ 4 on today's fixed exam order. This particular year happens to agree with the modern pattern here — the exam's own booklet also prints this question fourth — even though, as the earlier questions in this set show, 2013 didn't consistently follow what's now the standard ordering.

The Setup

  • SkyView holds:
    • private double[][] view — the reconstructed, properly oriented view of the sky
  • You're asked to write:
    • the constructor SkyView(int numRows, int numCols, double[] scanned) — builds view from telescope-order scan data
    • getAverage(int startRow, int endRow, int startCol, int endCol) — averages a rectangular section of view
  • "Telescope order" means the scanner alternates direction every row: row 0 is scanned left-to-right, row 1 right-to-left, row 2 left-to-right, and so on.

Part (a): Writing the SkyView(int numRows, int numCols, double[] scanned) constructor

The Rule, Broken Down

  1. Create view as a new 2D array with numRows rows and numCols columns.
  2. Scan data arrives in "telescope order" — direction alternates every row.
  3. Even-numbered rows (0, 2, 4, ...) were scanned left-to-right, so their data lands in view in the same left-to-right order it was received.
  4. Odd-numbered rows (1, 3, 5, ...) were scanned right-to-left, so their data must be reversed as it's stored, to end up in normal left-to-right reading order.

Step-by-Step Approach

  1. Allocate view as new double[numRows][numCols].
  2. Keep a single running index into scanned, starting at 0, that only ever moves forward — it tracks the order data was physically received, not the order it ends up stored in.
  3. Loop over each row. If the row number is even, fill its columns left to right (0 up to numCols - 1), pulling values from scanned in order.
  4. If the row number is odd, fill its columns right to left (numCols - 1 down to 0) instead, still pulling values from scanned in order.
  5. Every time a value is copied from scanned into view, advance the running index by one — regardless of which direction that row happens to be filling in.

The Code

public SkyView(int numRows, int numCols, double[] scanned)
{
    view = new double[numRows][numCols];
    int index = 0;

    for (int row = 0; row < numRows; row++)
    {
        if (row % 2 == 0)
        {
            for (int col = 0; col < numCols; col++)
            {
                view[row][col] = scanned[index];
                index++;
            }
        }
        else
        {
            for (int col = numCols - 1; col >= 0; col--)
            {
                view[row][col] = scanned[index];
                index++;
            }
        }
    }
}

Why Each Piece Matters

  • row % 2 == 0 is the direct Java translation of "alternating direction" — it's the single check that decides which way a row gets filled.
  • index only ever counts upward, one value at a time, no matter which direction the inner loop is walking through view's columns — that mirrors the fact that scanned itself is a flat, one-directional list of readings in the order the telescope actually received them.
  • The inner loop's bounds flip between the two branches (col = 0; col < numCols; col++ versus col = numCols - 1; col >= 0; col--), but the body inside each — copy scanned[index], then advance index — stays identical, so the two cases never need separately duplicated update logic.

Tracing the Example

Using the question's first example — numRows = 4, numCols = 3, scanned = [0.3, 0.7, 0.8, 0.4, 1.4, 1.1, 0.2, 0.5, 0.1, 1.6, 0.6, 0.9]:

Row Direction Columns filled Values taken from scanned Resulting row
0 left-to-right 0, 1, 2 0.3, 0.7, 0.8 0.3, 0.7, 0.8
1 right-to-left 2, 1, 0 0.4, 1.4, 1.1 1.1, 1.4, 0.4
2 left-to-right 0, 1, 2 0.2, 0.5, 0.1 0.2, 0.5, 0.1
3 right-to-left 2, 1, 0 1.6, 0.6, 0.9 0.9, 0.6, 1.6

This reproduces the exact view grid given in the question. The question's second example (numRows = 3, numCols = 2, scanned = [0.3, 0.7, 0.8, 0.4, 1.4, 1.1]) checks out the same way: row 0 (even) fills left-to-right as 0.3, 0.7; row 1 (odd) fills right-to-left, so scanned[2] = 0.8 goes to column 1 first and scanned[3] = 0.4 goes to column 0, giving row 1 = 0.4, 0.8; row 2 (even) fills left-to-right as 1.4, 1.1 — matching the question's second table exactly.

Common Mistakes to Avoid

  • Filling every row left-to-right regardless of its number — this ignores "telescope order" entirely and only happens to produce a correct result for the even rows.
  • Resetting index back to the start of each row's chunk instead of letting it count continuously across the whole array — scanned is one continuous stream in receipt order, not organized row by row.
  • Reversing the wrong set of rows (odd instead of even, or vice versa) — check the direction against a worked example rather than guessing the parity.
  • Allocating view with its dimensions swapped, new double[numCols][numRows], instead of new double[numRows][numCols].

Part (b): Writing getAverage(int startRow, int endRow, int startCol, int endCol)

The Rule, Broken Down

  1. Look only at the rectangular section of view running from startRow to endRow (inclusive) and startCol to endCol (inclusive).
  2. Add up every value in that section.
  3. Divide the sum by however many values were in the section.

Step-by-Step Approach

  1. Set up a running total (as a double) and a count of values seen, both starting at 0.
  2. Loop over every row from startRow to endRow, inclusive.
  3. Inside that, loop over every column from startCol to endCol, inclusive.
  4. Add each visited value to the total, and increment the count.
  5. After both loops finish, return the total divided by the count.

The Code

public double getAverage(int startRow, int endRow, int startCol, int endCol)
{
    double total = 0;
    int count = 0;

    for (int row = startRow; row <= endRow; row++)
    {
        for (int col = startCol; col <= endCol; col++)
        {
            total = total + view[row][col];
            count++;
        }
    }

    return total / count;
}

Why Each Piece Matters

  • Both loop bounds use <=, not < — the precondition explicitly states that endRow and endCol are themselves included in the section, not exclusive stopping points.
  • total is declared as a double from the start, since view itself holds doubles — unlike some other FRQs, there's no truncation risk here, so no separate cast is needed at the return statement.
  • count is tracked with a running variable instead of computed as (endRow - startRow + 1) * (endCol - startCol + 1) — both work, but incrementing a counter alongside the loop that's already visiting each cell is simpler to read and less prone to an off-by-one slip.

Tracing the Example

Using the question's own view grid and the call nightSky.getAverage(1, 2, 0, 1):

Row Col Value
1 0 1.1
1 1 1.4
2 0 0.2
2 1 0.5

Sum = 1.1 + 1.4 + 0.2 + 0.5 = 3.2. Count = 4. Average = 3.2 / 4 = 0.8 — matching the question's stated result exactly.

Common Mistakes to Avoid

  • Using < instead of <= in either loop, which silently skips endRow's row or endCol's column entirely.
  • Swapping row and column, writing view[col][row] instead of view[row][col] — this only happens to look correct on a square grid, and breaks on a rectangular one.
  • Declaring total as an int, which would truncate the final average the same way integer division does elsewhere — since view already holds doubles here, this particular mistake is easy to introduce out of habit even though it isn't forced by this problem the way it is in others.
  • Miscounting the number of values by forgetting the "+1" that comes from both bounds being inclusive.

Key Takeaways

  • Reconstructing a 1D array into a 2D shape with an alternating pattern is manageable with a single ever-advancing index into the 1D array, combined with a simple direction check (row % 2 == 0) per row.
  • Nested loops over a rectangular sub-section of a 2D array should use inclusive (<=) bounds whenever the problem specifies inclusive endpoints.
  • A "reconstruct the data" problem and a "summarize the data" problem often show up as the two halves of the same FRQ — one part transforms data into a usable shape, the other part reads back out of it.

Related FRQs