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
Stringto 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
WordMatchclass 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 againstsecretString findBetterGuess(String guess1, String guess2)— picks the better of two guesses
- The scoring rule: count how many times
guessoccurs as a substring ofsecret(overlapping matches count separately), then multiply that count by the square ofguess's length.
Part (a): Writing scoreGuess(String guess)
The Rule, Broken Down
- Look at every possible position in
secretwhere a substring the same length asguesscould start. - At each position, check whether that substring exactly matches
guess. - Count up every match — including matches that overlap each other.
- Multiply the final count by
guess.length()squared.
Step-by-Step Approach
- Start a counter at
0. - Loop
ifrom0up through the last position where aguess-length substring can still fit insidesecret— that'ssecret.length() - guess.length(). - At each
i, pull out the substring ofsecretstarting atiand running forguess.length()characters. - Compare that substring to
guesswith.equals(). If they match, increment the counter. - 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 exactlysecret.length() - guess.length(), so the loop has to include it, not stop before it.secret.substring(i, i + guess.length())— grabs exactly aguess-length window starting ati.substring(from, to)is exclusive ofto, soi + guess.length()correctly lands one character past the window's last character.i++(noti += 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==— comparingStringcontents 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 wheneverguessappears at the end ofsecret. - Stepping the loop by
guess.length()instead of1. 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(). TwoStringobjects 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
- Score both guesses using
scoreGuess. - If the scores differ, the guess with the higher score wins.
- If the scores are tied, the alphabetically greater guess wins instead.
Step-by-Step Approach
- Call
scoreGuessonce for each guess and store the results. - Compare the two scores. If they're different, return whichever guess produced the higher one.
- 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
scoreGuessinstead of re-implementing its logic — the problem explicitly says to assumescoreGuessworks 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
scoreGuesson the same guess twice, which would waste work (and would matter more ifscoreGuesswere expensive). guess1.compareTo(guess2) > 0—compareToreturns 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, sofindBetterGuess("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
scoreGuessa 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
compareToneeds 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.