CompSci.rocks
FRQcsapa

BoxOfCandy: 2023 FRQ 4

A step-by-step solution to the 2023 AP CSA FRQ 4 (BoxOfCandy), covering searching a 2D array of objects for null gaps, moving values within a column, and traversing a grid in reverse row order in Java.

A grid of candy where some spots are empty and others hold real objects sets up this AP Computer Science A free-response question — one method shuffles a single piece of candy up to the front of its column, the other hunts through the whole grid in a specific order to pull out one piece by name.

What This FRQ Tests

  • AP CSA units: Unit 8 (2D Array) and Unit 4 (Iteration)
  • Core skill: treating a 2D array of objects as a grid where a "gap" is represented by null, and searching or moving values around those gaps
  • Secondary skill: traversing a 2D array in a non-default order (last row to first, not first row to last) because the problem specifically defines it that way
  • Official category: "2D Array" — always FRQ 4 on the AP CSA exam

The Setup

  • The given Candy class has one method: String getFlavor() — you never modify this class.
  • BoxOfCandy holds one field: private Candy[][] box — a rectangular grid where each spot is either a Candy object or null (empty).
  • You're asked to write two methods:
    • boolean moveCandyToFirstRow(int col) — makes sure row 0 of column col holds a piece of candy, moving one up if needed; returns false only if the whole column is empty
    • Candy removeNextByFlavor(String flavor) — searches the grid from the last row to the first, left to right within each row, removes and returns the first matching piece of candy, or returns null if none is found

Part (a): Writing moveCandyToFirstRow(int col)

The Rule, Broken Down

  1. If row 0 of col already has candy, do nothing and return true.
  2. Otherwise, search the rest of column col (row 1 downward) for the first piece of candy found.
  3. If one is found, move it into row 0 of col, set its old spot to null, and return true.
  4. If the entire column has no candy anywhere, return false and leave box unchanged.

Step-by-Step Approach

  1. Check box[0][col] first. If it isn't null, the job is already done — return true immediately.
  2. Otherwise, loop down the column starting at row 1.
  3. At the first non-null spot found, copy that candy into box[0][col], set the original spot back to null, and return true right away.
  4. If the loop finishes without finding any candy, return false.

The Code

public boolean moveCandyToFirstRow(int col)
{
    if (box[0][col] != null)
    {
        return true;
    }

    for (int row = 1; row < box.length; row++)
    {
        if (box[row][col] != null)
        {
            box[0][col] = box[row][col];
            box[row][col] = null;
            return true;
        }
    }

    return false;
}

Why Each Piece Matters

  • The box[0][col] != null check comes first, on its own, before any loop runs. This is what makes an already-filled row 0 a true no-op — nothing gets rearranged that doesn't need to be.
  • The search loop starts at row = 1, not row = 0. Row 0 was already ruled out by the check above; re-checking it would be redundant (and re-reading a null there wouldn't cause a bug, but it wastes an iteration).
  • Both assignments — box[0][col] = box[row][col] and box[row][col] = null — are required. Skipping the second one would leave the same piece of candy appearing in two places at once, which breaks the "moved" behavior.
  • Returning immediately after the move (rather than continuing to search) means the first piece of candy found scanning downward is the one moved — matching one of the two equally acceptable outcomes the question describes for column 2.

Tracing the Example

Using the question's sample grid (rows 0–3, columns 0–2):

Row Col 0 Col 1 Col 2
0 (empty) "lime" (empty)
1 (empty) "orange" (empty)
2 (empty) (empty) "cherry"
3 (empty) "lemon" "grape"
  • moveCandyToFirstRow(0): box[0][0] is null. The loop checks rows 1, 2, and 3 of column 0 — all null. The loop finishes, returning false, and box is unchanged — matches the question.
  • moveCandyToFirstRow(1): box[0][1] already holds "lime" — the very first check catches this, returning true immediately with nothing changed — matches the question.
  • moveCandyToFirstRow(2): box[0][2] is null. The loop checks row 1 (null), then row 2, which holds "cherry" — the first match. "cherry" moves to box[0][2], box[2][2] becomes null, and the method returns true. This produces exactly one of the two grids the question accepts as correct — the one where "cherry" (not "grape") ends up in row 0.

Common Mistakes to Avoid

  • Forgetting to null out the candy's old position after moving it — this duplicates the object instead of moving it.
  • Starting the search loop at row = 0 instead of row = 1, or forgetting the initial "already filled" check entirely, and instead unconditionally overwriting row 0 with whatever the loop finds first (which could destroy an already-present piece of candy).
  • Continuing to search after finding a match instead of returning right away — not incorrect in principle, but it changes which piece of candy gets picked when a column has more than one, and adds unnecessary work.

Part (b): Writing removeNextByFlavor(String flavor)

The Rule, Broken Down

  1. Search the grid starting from the last row, moving left to right across each row.
  2. After finishing a row, move to the row above it, again left to right — continuing until either a match is found or the entire grid has been checked.
  3. The first candy whose flavor matches flavor gets removed (its spot set to null) and returned.
  4. If nothing matches anywhere, return null and leave box completely unchanged.

Step-by-Step Approach

  1. Loop the row index backwards, starting at the last row (box.length - 1) down to 0.
  2. Inside that, loop the column index forwards as usual, from 0 up to the row's length.
  3. At each spot, first check it isn't null (an empty spot has no flavor to check), then compare its flavor to flavor.
  4. On a match: save a reference to that candy, set the spot to null, and return the saved reference immediately.
  5. If both loops finish with no match, return null.

The Code

public Candy removeNextByFlavor(String flavor)
{
    for (int row = box.length - 1; row >= 0; row--)
    {
        for (int col = 0; col < box[row].length; col++)
        {
            if (box[row][col] != null && box[row][col].getFlavor().equals(flavor))
            {
                Candy found = box[row][col];
                box[row][col] = null;
                return found;
            }
        }
    }

    return null;
}

Why Each Piece Matters

  • The outer loop runs backwards (row = box.length - 1; row >= 0; row--) because the problem specifically defines the search order as starting from the last row — this is the opposite of the usual top-to-bottom 2D array traversal, and has to be written deliberately, not assumed.
  • The inner loop still runs forwards, left to right within each row — only the row order is reversed, not the column order.
  • box[row][col] != null is checked before .getFlavor() is called, and specifically before, using &&'s short-circuit behavior. Calling .getFlavor() on a null reference would throw a NullPointerException — this check has to come first in the condition, not after.
  • .equals(flavor), never ==, to compare the two Strings by their characters rather than by object identity.
  • Saving the candy into found before nulling out its spot. Setting box[row][col] = null first would lose the only reference to the object before it could be returned.

Tracing the Example

Using the question's sample grid (rows 0–2, columns 0–4):

Row Col 0 Col 1 Col 2 Col 3 Col 4
0 "lime" "lime" (empty) "lemon" (empty)
1 "orange" (empty) (empty) "lime" "lime"
2 "cherry" (empty) "lemon" (empty) "orange"
  • removeNextByFlavor("cherry"): the search starts at row 2 (the last row). Column 0 of row 2 is "cherry" — an immediate match. That spot is set to null, and the "cherry" candy is returned — matches the question (row 2, column 0).
  • removeNextByFlavor("lime") (called next, on the updated grid): row 2 has no "lime" anywhere (cherry's spot is now empty, the rest don't match). Row 1: column 0 is "orange" (no match), columns 1–2 are empty, column 3 is "lime" — a match. That spot is nulled and returned — matches the question (row 1, column 3).
  • removeNextByFlavor("grape") (called last, on the updated grid): rows 2, 1, and 0 are all searched in full with no "grape" found anywhere. The method returns null, and box is left unchanged — matches the question.

Common Mistakes to Avoid

  • Traversing rows in the normal 0 to box.length - 1 order instead of backwards — this is the single most important detail in this method, and it's easy to default to the "usual" traversal out of habit.
  • Calling .getFlavor() before checking for null, or combining the checks with || instead of && — either throws a NullPointerException the first time an empty spot is reached.
  • Comparing flavors with == instead of .equals(). Two separate String objects holding the same characters are not the same object in memory, so == can incorrectly report "no match" even when the flavors read identically.
  • Forgetting to set the found spot back to null after removing it — leaving the same candy object "in the box" while also handing it back to the caller.

Key Takeaways

  • A 2D array of objects can represent a grid with gaps just by allowing null in some spots — searching it means checking for null before doing anything else with a cell's value.
  • A traversal order isn't always "top row to bottom row, left to right" — read the problem's description of the search order carefully, since it can require looping a dimension backwards.
  • "Find, remove, and return the first match" only needs one loop pass: save a reference to the match, clear its spot, and return immediately — no separate cleanup step needed afterward.

Related FRQs