LightBoard: 2019 FRQ 4
A step-by-step solution to the 2019 AP CSA FRQ 4 (LightBoard), covering random 2D array initialization at a fixed probability and column-wise traversal in Java.
A grid of on/off lights is a simple enough idea, but this AP Computer Science A free-response question packs two classic 2D array skills into it — randomly filling a grid so a specific percentage of cells end up on, then scanning down individual columns to answer a rule that depends on the whole column at once.
What This FRQ Tests
- AP CSA units: Unit 8 (2D Array), Unit 4 (Iteration)
- Core skill: nested loops to fill every cell of a 2D array with an independently random value
- Secondary skill: scanning an entire column of a 2D array to compute a count before deciding what a single cell in that column should return
- Official category: "2D Array" — always FRQ 4 on the AP CSA exam
The Setup
LightBoardholds:private boolean[][] lights— a 2D array wheretruemeans a light is on,falsemeans off
- You're asked to write:
- The constructor
LightBoard(int numRows, int numCols)— buildslightsand sets each light on with 40% probability evaluateLight(int row, int col)— computes and returns a light's status based on how many lights are on elsewhere in its column
- The constructor
Part (a): Writing the Constructor
The Rule, Broken Down
- Allocate
lightswith the given number of rows and columns. - Every single cell, independently of every other cell, must end up
truewith exactly a 40% chance (andfalsethe other 60%).
Step-by-Step Approach
- Create the array:
lights = new boolean[numRows][numCols]. - Loop over every row, and inside that, every column.
- For each cell, generate a random
doublewithMath.random()(always in[0.0, 1.0)) and compare it to0.4. - Store the result of that comparison directly — it's
trueexactly when the random value fell in the lowest 40% of the range.
The Code
public LightBoard(int numRows, int numCols)
{
lights = new boolean[numRows][numCols];
for (int r = 0; r < numRows; r++)
{
for (int c = 0; c < numCols; c++)
{
lights[r][c] = Math.random() < 0.4;
}
}
}
Why Each Piece Matters
Math.random()returns a value uniformly distributed across[0.0, 1.0)— every sub-range of that interval is exactly as likely as any other equally-sized sub-range.- The interval
[0.0, 0.4)— the values that makeMath.random() < 0.4true — takes up exactly 40% of the full[0.0, 1.0)range, so the comparison is true with exactly 40% probability, independently for every cell. lights[r][c] = Math.random() < 0.4stores the boolean result of the comparison directly — there's no need for anif/elsethat separately assignstrueorfalse, since the comparison itself already produces a boolean value.- Nested loops are required, not a single loop, because
lightsis two-dimensional — the outer loop walks rows, the inner loop walks every column within that row.
Common Mistakes to Avoid
- Comparing against the wrong side, e.g.
Math.random() > 0.4orMath.random() < 0.6— either of these gives a 60% chance of being on, not 40%. - Writing an unnecessary
if (Math.random() < 0.4) { lights[r][c] = true; } else { lights[r][c] = false; }. This works, but it's more code than the direct boolean assignment for no benefit. - Using only one loop, or reusing the same index variable for both dimensions. Every one of the
numRows * numColscells needs its own independent random check.
Part (b): Writing evaluateLight(int row, int col)
The Rule, Broken Down
- If the light at
(row, col)is currently on: returnfalseif the number of lights on in that same column — counting the current light itself — is even. - If the light is currently off: return
trueif the number of lights on in that column is divisible by three. - In every other case, just return the light's current, unchanged status.
Step-by-Step Approach
- Count how many lights are on somewhere in column
col, by looping down every row of that one column. - Check the current light's own status first, since the rule branches completely differently depending on whether it's on or off.
- If it's on and the count is even, return
false. - Otherwise, if it's off and the count is divisible by 3, return
true. - Otherwise, return
lights[row][col]itself — its current, unmodified status.
The Code
public boolean evaluateLight(int row, int col)
{
int count = 0;
for (int r = 0; r < lights.length; r++)
{
if (lights[r][col])
{
count++;
}
}
if (lights[row][col] && count % 2 == 0)
{
return false;
}
else if (!lights[row][col] && count % 3 == 0)
{
return true;
}
else
{
return lights[row][col];
}
}
Why Each Piece Matters
for (int r = 0; r < lights.length; r++)walks down every row while holdingcolfixed — this is exactly how you traverse a single column of a 2D array, sincelights[r][col]only ever changes the row index.lights.lengthgives the number of rows, so the loop covers every row — includingrowitself, which is exactly what the rule means by "including the current light."- The
&&in each condition requires both the on/off status and the count check to agree before returning early — a light that's on with an odd count, or off with a count not divisible by 3, is meant to fall through to the finalelseon purpose. - The final
else return lights[row][col]handles rule 3 ("otherwise, return the light's current status") without needing to store or recompute anything extra — the array already holds that value.
Tracing the Example
Using the 7×5 board from the question (true = on, false = off), with column counts computed by scanning each column top to bottom:
| Column | Values, rows 0-6 (top to bottom) | Count on |
|---|---|---|
| 0 | T, T, T, T, T, T, F | 6 |
| 1 | T, F, F, F, F, T, F | 2 |
| 3 | T, T, T, F, F, T, F | 4 |
| 4 | T, F, T, T, T, T, F | 5 |
Checking each sample call from the question against the code:
sim.evaluateLight(0, 3):lights[0][3]istrue, column 3's count is 4 (even) → on and even → returns false, matching the question.sim.evaluateLight(6, 0):lights[6][0]isfalse, column 0's count is 6 (divisible by 3) → off and divisible by 3 → returns true, matching the question.sim.evaluateLight(4, 1):lights[4][1]isfalse, column 1's count is 2 (not divisible by 3) → falls through to the finalelse→ returnslights[4][1], which is false, matching the question.sim.evaluateLight(5, 4):lights[5][4]istrue, column 4's count is 5 (odd, not even) → falls through to the finalelse→ returnslights[5][4], which is true, matching the question.
Common Mistakes to Avoid
- Swapping
lights.lengthforlights[row].length(or vice versa) — here they matter for different reasons:lights.lengthcorrectly gives the number of rows to scan down a column, whilelights[row].lengthwould give the number of columns, the wrong dimension entirely for this count. - Forgetting to count the current light itself. The rule explicitly says "including the current light," so the counting loop must run over every row without skipping
row. - Combining the on/off check and the count check with
||instead of&&. This would make rules 1 and 2 fire in situations where they shouldn't. - Checking a count condition without first confirming on/off status — e.g., testing
count % 2 == 0as the very first check, which would incorrectly apply rule 1's logic to a light that's actually off.
Key Takeaways
static double random()returning a value in[0.0, 1.0)means "X% probability" almost always translates directly toMath.random() < X / 100.0, or the literal decimal form of X%.- Traversing a single column of a 2D array means holding the column index fixed and looping the row index from
0tolights.length - 1— the mirror image of the row-traversal pattern from 2022'sData.countIncreasingCols(). - When a rule branches into multiple distinct cases ("if on, do X; if off, do Y; otherwise Z"), check the mutually exclusive condition first, then the secondary condition within each branch, and always keep an explicit final "otherwise" case.
Need help preparing for the AP exam?
FRQs are one of the toughest parts of the AP CS exam. I offer 1-on-1 tutoring to help you work through practice problems, tighten up your responses, and build the confidence to earn full credit on exam day.
Book a tutoring session →