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
SkyViewholds: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)— buildsviewfrom telescope-order scan data getAverage(int startRow, int endRow, int startCol, int endCol)— averages a rectangular section ofview
- the constructor
- "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
- Create
viewas a new 2D array withnumRowsrows andnumColscolumns. - Scan data arrives in "telescope order" — direction alternates every row.
- Even-numbered rows (0, 2, 4, ...) were scanned left-to-right, so their data lands in
viewin the same left-to-right order it was received. - 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
- Allocate
viewasnew double[numRows][numCols]. - 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. - Loop over each row. If the row number is even, fill its columns left to right (
0up tonumCols - 1), pulling values fromscannedin order. - If the row number is odd, fill its columns right to left (
numCols - 1down to0) instead, still pulling values fromscannedin order. - Every time a value is copied from
scannedintoview, 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 == 0is the direct Java translation of "alternating direction" — it's the single check that decides which way a row gets filled.indexonly ever counts upward, one value at a time, no matter which direction the inner loop is walking throughview's columns — that mirrors the fact thatscanneditself 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++versuscol = numCols - 1; col >= 0; col--), but the body inside each — copyscanned[index], then advanceindex— 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
indexback to the start of each row's chunk instead of letting it count continuously across the whole array —scannedis 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
viewwith its dimensions swapped,new double[numCols][numRows], instead ofnew double[numRows][numCols].
Part (b): Writing getAverage(int startRow, int endRow, int startCol, int endCol)
The Rule, Broken Down
- Look only at the rectangular section of
viewrunning fromstartRowtoendRow(inclusive) andstartColtoendCol(inclusive). - Add up every value in that section.
- Divide the sum by however many values were in the section.
Step-by-Step Approach
- Set up a running total (as a
double) and a count of values seen, both starting at 0. - Loop over every row from
startRowtoendRow, inclusive. - Inside that, loop over every column from
startColtoendCol, inclusive. - Add each visited value to the total, and increment the count.
- 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 thatendRowandendColare themselves included in the section, not exclusive stopping points. totalis declared as adoublefrom the start, sinceviewitself holdsdoubles — unlike some other FRQs, there's no truncation risk here, so no separate cast is needed at the return statement.countis 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 skipsendRow's row orendCol's column entirely. - Swapping row and column, writing
view[col][row]instead ofview[row][col]— this only happens to look correct on a square grid, and breaks on a rectangular one. - Declaring
totalas anint, which would truncate the final average the same way integer division does elsewhere — sinceviewalready holdsdoubles 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.