Crossword: 2016 FRQ 3
A step-by-step solution to the 2016 AP CSA FRQ 3 (Crossword), covering checking a square's neighbors in a 2D array and building a full 2D array of objects with row-major numbering in Java.
Numbering a crossword grid the way real puzzle books do turns out to be a neat little exercise in checking a 2D array's neighbors — this AP Computer Science A free-response question has you decide which squares deserve a number before building the whole labeled grid from scratch.
What This FRQ Tests
- AP CSA units: Unit 8 (2D Array) and Unit 4 (Iteration)
- Core skill: checking a cell's up and left neighbors in a 2D array without ever indexing off the edge of the grid
- Secondary skill: building an entire 2D array of objects while a counter keeps incrementing across nested loops
- Official category: "2D Array" — the category the modern fixed ordering assigns to FRQ 4, but the 2016 booklet prints this question as FRQ 3. Paired with this same year's FRQ 4 (
StringFormatter, which tests Array/ArrayList — normally FRQ 3's slot), the two categories are effectively swapped relative to the modern pattern, the same kind of deviation documented for 2018.
The Setup
- A
Squareis built withSquare(boolean isBlack, int num)— already implemented, not something you need to write. - A
Crosswordholdsprivate Square[][] puzzle, wherepuzzle[r][c]is the square at rowr, columnc. - The crossword labeling rule: a square gets a positive number if and only if it's white, and it either has no white square directly above it, or no white square directly to its left (or both). Being in the top row counts as "no white square above"; being in the leftmost column counts as "no white square to the left."
- Labeled squares are numbered with consecutive positive integers in row-major order, starting at 1. Every other square (black squares, and white squares that don't qualify) gets the number
0. - Two things to write:
private boolean toBeLabeled(int r, int c, boolean[][] blackSquares)(part a), and theCrossword(boolean[][] blackSquares)constructor (part b).
Part (a): Writing toBeLabeled
The Rule, Broken Down
blackSquares[r][c]tells you whether square(r, c)is black — only white squares can ever be labeled.- "No white square above" is true when
r == 0(nothing is above the top row at all) orblackSquares[r - 1][c]istrue. - "No white square to the left" is true when
c == 0orblackSquares[r][c - 1]istrue. - The square is labeled when it's white and at least one of those two neighbor conditions holds.
Step-by-Step Approach
- If the square itself is black, it can never be labeled — return
falseimmediately, without even looking at its neighbors. - Otherwise, check whether there's no white square above.
- Also check whether there's no white square to the left.
- Return
trueif either of those two checks came back true.
The Code
private boolean toBeLabeled(int r, int c, boolean[][] blackSquares)
{
if (blackSquares[r][c])
{
return false;
}
boolean noWhiteAbove = (r == 0) || blackSquares[r - 1][c];
boolean noWhiteToLeft = (c == 0) || blackSquares[r][c - 1];
return noWhiteAbove || noWhiteToLeft;
}
Why Each Piece Matters
- Returning
falseimmediately for a black square means the rest of the method never has to reason about "what are a black square's neighbors" — black squares are simply excluded before that question is ever asked. (r == 0) || blackSquares[r - 1][c]relies on Java's short-circuit evaluation:||checks its left side first, and only evaluates the right side if the left side wasfalse. Sincer == 0is checked first,blackSquares[r - 1][c]is never evaluated whenris0— which is exactly what avoids anArrayIndexOutOfBoundsExceptionfrom indexing row-1. The same logic protectsblackSquares[r][c - 1]whenc == 0.- "No white square above" also covers a genuine black square directly above the same way it covers being in the top row — both mean nothing white is blocking a new labeled entry from starting there.
Tracing the Example
The official diagram illustrates the rule graphically rather than as a text table, so the exact grid isn't reproduced cell-by-cell here — but it includes four explicit callouts that exercise every branch of this method:
- Two squares are annotated "Labeled because no white square above and no white square to the left." For these, both
noWhiteAboveandnoWhiteToLeftaretrueat once, so the final||is true for two independent reasons simultaneously. - One square is annotated "Labeled because no white square above" alone — meaning a white square does sit to its left (
noWhiteToLeftisfalsethere), but it's still labeled, since the||only needs one side to be true. - One square is annotated "Labeled because no white square to the left" alone — the mirror case: a white square sits above it, but it's still labeled because of its left side.
- Two squares are annotated "Unlabeled." A square ends up unlabeled either because it's black (the very first check returns
falsebefore any neighbor is examined), or because it's white but has both a white square above it and a white square to its left (bothnoWhiteAboveandnoWhiteToLeftcome backfalse, so the||isfalsetoo). Either way, the method above correctly returnsfalsefor it.
Together, these four callouts confirm all three ways a square can end up labeled (above-only, left-only, or both) plus the exclusion of unqualified squares — and the method matches the documented result in every case.
Part (b): Writing the Crossword Constructor
The Rule, Broken Down
puzzlemust end up with the exact same dimensions asblackSquares.- Every cell gets a new
Squareobject built with the correctisBlackvalue. - White cells that satisfy
toBeLabeledget consecutive positive numbers, starting at 1, assigned in row-major order. - Every other cell (black cells, and white cells that don't qualify) gets the number
0.
Step-by-Step Approach
- Allocate
puzzleasnew Square[blackSquares.length][blackSquares[0].length]— matching dimensions exactly, rather than hardcoding any size. - Declare a running counter,
nextNumber, starting at1, outside both loops so it persists across the entire grid, not just one row. - Loop over every row
r, then every columncwithin that row — nested loops naturally visit cells in row-major order. - For each cell: if it's black, create
Square(true, 0). If it's white andtoBeLabeledreturnstrue, createSquare(false, nextNumber)and then incrementnextNumber. Otherwise, createSquare(false, 0). - Store the new
Squareintopuzzle[r][c].
The Code
public Crossword(boolean[][] blackSquares)
{
puzzle = new Square[blackSquares.length][blackSquares[0].length];
int nextNumber = 1;
for (int r = 0; r < blackSquares.length; r++)
{
for (int c = 0; c < blackSquares[0].length; c++)
{
if (blackSquares[r][c])
{
puzzle[r][c] = new Square(true, 0);
}
else if (toBeLabeled(r, c, blackSquares))
{
puzzle[r][c] = new Square(false, nextNumber);
nextNumber++;
}
else
{
puzzle[r][c] = new Square(false, 0);
}
}
}
}
Why Each Piece Matters
new Square[blackSquares.length][blackSquares[0].length]copiesblackSquares's exact dimensions instead of assuming any particular size, so the constructor works for a grid of any shape.nextNumberis declared before both loops, not reset inside either one — if it were reinitialized inside the outer (row) loop, every row would incorrectly restart numbering at 1 instead of continuing across the whole grid.else if (toBeLabeled(...))only runs for cells already known to be white — since black cells are handled in the first branch, this skips a redundant re-check ofblackSquares[r][c]thattoBeLabeledwould otherwise repeat internally.- The nested loop structure itself is row-major order — every column of one row finishes before the next row starts, so no separate sorting step is ever needed to satisfy that requirement.
Common Mistakes to Avoid
- Declaring
int nextNumber = 1inside the outer (row) loop instead of before it — this resets numbering back to 1 at the start of every row instead of continuing it across the whole grid. - Using
blackSquares.lengthfor the number of columns instead ofblackSquares[0].length— grid dimensions are rows-then-columns, and mixing these up is common enough that it's one of the official 2016 scoring guideline's named penalties ("Usesarray[].lengthinstead ofarray[num].length"). - Forgetting to increment
nextNumberafter creating a labeledSquare— every labeled square from that point on would incorrectly get the same number. - Calling
toBeLabeledon a square already known to be black and trusting its result without ever checkingblackSquares[r][c]directly — this still works (sincetoBeLabeledreturnsfalsefor black squares), but skips the more direct, more efficient check.
Key Takeaways
- Checking edge conditions (
r == 0,c == 0) before indexing a neighboring cell, using short-circuit||or&&, is the standard way to avoid anArrayIndexOutOfBoundsExceptionwhen examining a 2D array's neighbors. - A counter that has to persist across an entire nested loop — like row-major numbering across a whole grid — must be declared outside both loops, never reset anywhere inside them.
- Splitting "does this square qualify" into its own method (
toBeLabeled) and calling it from inside the constructor keeps the labeling rule's logic in exactly one place, even while it's being used to build an entire 2D array of objects.