CompSci.rocks
FRQcsapa

WordMatch: 2021 FRQ 1

A step-by-step solution to the 2021 AP CSA FRQ 1 (WordMatch), covering substring counting, overlapping matches, and comparing scores in Java.

A word-guessing game with a hidden secret string sits behind this AP Computer Science A free-response question — you score a guess by how often it turns up inside the secret word, then decide which of two guesses actually scored better.

What This FRQ Tests

  • AP CSA units: Unit 3 (Boolean Expressions and if Statements), Unit 4 (Iteration), and Unit 5 (Writing Classes/methods)
  • Core skill: looping across every possible starting position in a String to count overlapping substring matches
  • Secondary skill: comparing two computed results and breaking a tie with a secondary rule
  • Official category: "Methods and Control Structures" — always FRQ 1 on the AP CSA exam

The Setup

  • The given WordMatch class has:
    • private String secret — the hidden string, never exposed directly
    • A constructor that stores the secret word
  • You're asked to write two methods:
    • int scoreGuess(String guess) — scores one guess against secret
    • String findBetterGuess(String guess1, String guess2) — picks the better of two guesses
  • The scoring rule: count how many times guess occurs as a substring of secret (overlapping matches count separately), then multiply that count by the square of guess's length.

Part (a): Writing scoreGuess(String guess)

The Rule, Broken Down

  1. Look at every possible position in secret where a substring the same length as guess could start.
  2. At each position, check whether that substring exactly matches guess.
  3. Count up every match — including matches that overlap each other.
  4. Multiply the final count by guess.length() squared.

Step-by-Step Approach

  1. Start a counter at 0.
  2. Loop i from 0 up through the last position where a guess-length substring can still fit inside secret — that's secret.length() - guess.length().
  3. At each i, pull out the substring of secret starting at i and running for guess.length() characters.
  4. Compare that substring to guess with .equals(). If they match, increment the counter.
  5. After the loop, return count * guess.length() * guess.length().

The loop bound is the trickiest part: it has to allow the last valid starting index, not stop one short of it, and it has to allow overlapping matches, which is exactly why you advance i by just 1 each time instead of jumping ahead by guess.length().

The Code

public int scoreGuess(String guess)
{
    int count = 0;

    for (int i = 0; i <= secret.length() - guess.length(); i++)
    {
        if (secret.substring(i, i + guess.length()).equals(guess))
        {
            count++;
        }
    }

    return count * guess.length() * guess.length();
}

Why Each Piece Matters

  • i <= secret.length() - guess.length() — this is a <=, not a <. The last legal starting index is exactly secret.length() - guess.length(), so the loop has to include it, not stop before it.
  • secret.substring(i, i + guess.length()) — grabs exactly a guess-length window starting at i. substring(from, to) is exclusive of to, so i + guess.length() correctly lands one character past the window's last character.
  • i++ (not i += guess.length()) — advancing one character at a time is what lets overlapping matches (like "aa" inside "aaaa") all get counted, instead of skipping past a match that starts partway through the previous one.
  • .equals(), not == — comparing String contents always uses .equals(); == would compare object identity instead.
  • Squaring guess.length() — this rewards longer, more specific guesses much more heavily than short ones, since the score grows with the square of the length, not just the length itself.

Tracing the Example

Using WordMatch game = new WordMatch("mississippi") (length 11):

guess Valid start positions checked Matches found Score
"i" 0 through 10 positions 1, 4, 7, 10 → 4 4 × 1² = 4
"iss" 0 through 8 positions 1, 4 → 2 2 × 3² = 18
"issipp" 0 through 5 position 4 → 1 1 × 6² = 36
"mississippi" 0 through 0 position 0 → 1 1 × 11² = 121

All four results match the table given in the question, including the two overlapping "i" matches and the two overlapping "iss" matches inside "mississippi".

Common Mistakes to Avoid

  • Using i < secret.length() - guess.length() instead of <=. This silently drops the very last valid starting position, undercounting matches whenever guess appears at the end of secret.
  • Stepping the loop by guess.length() instead of 1. This finds only non-overlapping matches, which breaks examples like "aa" inside "aaaabb" (which should count 3 overlapping occurrences, not 2).
  • Comparing with == instead of .equals(). Two String objects holding identical characters aren't guaranteed to be the same object in memory.
  • Forgetting to square the length, or squaring the count instead of the length. Re-read the formula carefully: it's (occurrences) × (length)², not the other way around.

Part (b): Writing findBetterGuess(String guess1, String guess2)

The Rule, Broken Down

  1. Score both guesses using scoreGuess.
  2. If the scores differ, the guess with the higher score wins.
  3. If the scores are tied, the alphabetically greater guess wins instead.

Step-by-Step Approach

  1. Call scoreGuess once for each guess and store the results.
  2. Compare the two scores. If they're different, return whichever guess produced the higher one.
  3. If they're equal, fall back to comparing the guesses themselves with .compareTo() and return the alphabetically greater one.

The Code

public String findBetterGuess(String guess1, String guess2)
{
    int score1 = scoreGuess(guess1);
    int score2 = scoreGuess(guess2);

    if (score1 != score2)
    {
        if (score1 > score2)
        {
            return guess1;
        }
        else
        {
            return guess2;
        }
    }
    else
    {
        if (guess1.compareTo(guess2) > 0)
        {
            return guess1;
        }
        else
        {
            return guess2;
        }
    }
}

Why Each Piece Matters

  • Calling scoreGuess instead of re-implementing its logic — the problem explicitly says to assume scoreGuess works correctly and to use it; duplicating its logic here would be redundant and risk introducing a second bug.
  • Storing both scores in variables before comparing — this avoids calling scoreGuess on the same guess twice, which would waste work (and would matter more if scoreGuess were expensive).
  • guess1.compareTo(guess2) > 0compareTo returns a positive value when the calling string is alphabetically greater than the argument, so this exactly matches "return the alphabetically greater guess."

Tracing the Example

Using WordMatch game = new WordMatch("concatenation"):

  • scoreGuess("ten") → 9, scoreGuess("nation") → 36 — different scores, so findBetterGuess("ten", "nation") returns whichever had the higher score: "nation" — matches.
  • scoreGuess("con") → 9, scoreGuess("cat") → 9 — tied scores, so the tie-breaker kicks in: "con".compareTo("cat") compares character by character. The first characters ('c' vs 'c') match, so it moves to the second character: 'o' comes after 'a' alphabetically, so the result is positive, meaning "con" is alphabetically greater. findBetterGuess("con", "cat") returns "con" — matches.

Common Mistakes to Avoid

  • Calling scoreGuess a third or fourth time unnecessarily inside the comparison logic, instead of reusing the two stored values.
  • Getting the tie-breaker direction backwards — the rule asks for the alphabetically greater guess, which means compareTo needs to return a value greater than 0, not less than 0.
  • Forgetting the tie case entirely and only handling "scores are different."

Notes: A Faster Way to Count Matches

The main solution checks every single starting position with substring and .equals(), which is easy to reason about but does some repeated work. A first-year (non-AP) Java course often teaches the two-argument overload of indexOf that lets you search starting from a given position — it isn't listed on the AP Quick Reference sheet (only the one-argument indexOf(String str) is), but that doesn't make it disallowed on the real exam:

public int scoreGuess(String guess)
{
    int count = 0;
    int index = secret.indexOf(guess);

    while (index != -1)
    {
        count++;
        index = secret.indexOf(guess, index + 1);
    }

    return count * guess.length() * guess.length();
}
  • secret.indexOf(guess, index + 1) jumps straight to the next occurrence starting after the previous one, instead of checking every single position one at a time.
  • This is completely valid Java to write on the real exam — the only real tradeoff is that this particular two-argument overload isn't printed on the Quick Reference sheet, so you can't look its exact behavior up mid-exam if you're unsure how it handles the starting index.

Key Takeaways

  • Counting overlapping substring matches means advancing your search position by 1, never by the length of the thing you're searching for.
  • A loop bound involving subtraction (secret.length() - guess.length()) is a strong signal to double-check whether it should be < or <= — off-by-one errors hide exactly there.
  • "Compare two computed values, then break ties with a secondary rule" is a two-step pattern: compute both values first, then branch on equality before branching on which is larger.

Related FRQs