CompSci.rocks
FRQcsapa

RouteCipher: 2011 FRQ 4

A step-by-step solution to the 2011 AP CSA FRQ 4 (RouteCipher), covering filling a 2D array in row-major order and repeatedly encrypting fixed-size chunks of a longer message in Java.

Turning a message into a coded jumble by reading it back out in a different order is the trick behind this AP Computer Science A free-response question — you fill a 2D grid one row at a time, then chain that process together to handle a message longer than a single grid can hold.

What This FRQ Tests

  • AP CSA units: Unit 8 (2D Array) and Unit 4 (Iteration)
  • Core skill: filling a 2D array in row-major order using a single running index computed from row and column
  • Secondary skill: repeatedly processing fixed-size chunks of a longer string until the whole thing is consumed, handling a shorter final chunk
  • Official category: "2D Array" — always FRQ 4 on the AP CSA exam

The Setup

  • The given RouteCipher class has:
    • private String[][] letterBlock — instantiated in the constructor
    • private int numRows and private int numCols
    • private String encryptBlock() — already implemented; extracts the encrypted text from letterBlock in column-major order (assume it's correct — you're told not to reimplement it)
  • You're asked to write two methods:
    • private void fillBlock(String str) — fills letterBlock in row-major order
    • public String encryptMessage(String message) — encrypts a message of any length by repeatedly calling fillBlock and encryptBlock on consecutive chunks

Part (a): Writing fillBlock(String str)

The Rule, Broken Down

  1. Fill letterBlock in row-major order: the first row left-to-right, then the second row left-to-right, and so on.
  2. Each cell gets a one-character substring pulled from str.
  3. If str is too short to fill every cell, pad the remaining cells with "A".
  4. If str is too long, ignore whatever characters don't fit.

Step-by-Step Approach

  1. Loop over every row r.
  2. Within each row, loop over every column c.
  3. Compute the row-major position k = r * numCols + c — this is str's matching index for this cell.
  4. If k is still a valid index into str, pull out that one character as a String.
  5. Otherwise, str has run out — fill the cell with "A" instead.

The Code

private void fillBlock(String str)
{
    for (int r = 0; r < numRows; r++)
    {
        for (int c = 0; c < numCols; c++)
        {
            int k = r * numCols + c;

            if (k < str.length())
            {
                letterBlock[r][c] = str.substring(k, k + 1);
            }
            else
            {
                letterBlock[r][c] = "A";
            }
        }
    }
}

Why Each Piece Matters

  • k = r * numCols + c is the standard formula for converting a 2D row/column position into the equivalent 1D position in row-major order: every complete row before this one contributes numCols characters, plus c more within the current row.
  • str.substring(k, k + 1) — the exact expression the question itself suggests — pulls out a single character as a one-character String, matching the declared type String[][] letterBlock (a char wouldn't fit that array type).
  • The k < str.length() check happens before calling substring, since calling substring(k, k + 1) with k past the end of str would throw an exception instead of just being treated as "ran out of characters."

Tracing the Example

With letterBlock sized 3 rows by 5 columns and str = "Meet at noon" (length 12, so 3 cells short of the full 15):

Row k values Characters Result
0 0-4 M, e, e, t, (space) "M" "e" "e" "t" " "
1 5-9 a, t, (space), n, o "a" "t" " " "n" "o"
2 10-14 o, n, (out of range) ×3 "o" "n" "A" "A" "A"

This matches the question's table exactly, including the three padded "A" cells at the end.

With the longer str = "Meet at midnight" (length 16, one character more than the 15 available cells), the same formula fills every cell from the string and simply never reaches the trailing "t" — row 2 comes out "d" "n" "i" "g" "h", exactly as the question shows, with the final character silently ignored.

Common Mistakes to Avoid

  • Swapping the row/column formula to c * numRows + r, which fills the array in column-major order instead of row-major.
  • Using k <= str.length() instead of <, which would try to read one character past the end of str on the boundary case.
  • Writing 'A' (a char) instead of "A" (a String) for the padding — letterBlock is a String[][], so the padding value has to be a String literal.
  • Assuming leftover cells from a previous call stay untouched. They don't need special handling here — the loop always runs over every one of the numRows * numCols cells on every call, so old data is naturally overwritten either with a new character or a fresh "A".

Part (b): Writing encryptMessage(String message)

The Rule, Broken Down

  1. If message is empty, return the empty string immediately.
  2. Otherwise, split message into consecutive, non-overlapping chunks, each up to numRows * numCols characters long.
  3. For each chunk — the last one may be shorter — call fillBlock, then encryptBlock, and append the result to a running total.
  4. Once the whole message has been consumed, return the accumulated result.

Step-by-Step Approach

  1. Compute the block size: numRows * numCols.
  2. Track how much of message has been consumed so far, starting at 0.
  3. While there's more of message left: figure out where the current chunk ends (either a full block size ahead, or the end of message, whichever comes first), pull out that chunk, call fillBlock on it, call encryptBlock, and append its result.
  4. Advance the "consumed so far" position by a full block size each time — even on the final, partial chunk — since the loop's own condition will correctly stop it from running again.
  5. Return the accumulated result.

The Code

public String encryptMessage(String message)
{
    String result = "";
    int blockSize = numRows * numCols;
    int start = 0;

    while (start < message.length())
    {
        int end = start + blockSize;

        if (end > message.length())
        {
            end = message.length();
        }

        String piece = message.substring(start, end);
        fillBlock(piece);
        result = result + encryptBlock();
        start = start + blockSize;
    }

    return result;
}

Why Each Piece Matters

  • start < message.length() as the loop condition, checked before anything else runs, is exactly what makes an empty message correctly return "" — the loop body never executes at all, so result stays empty.
  • Clamping end down to message.length() when a full block would overshoot lets the very last chunk be shorter than a complete block, since substring can't read past the end of the string.
  • Advancing start by the full blockSize, not by the chunk's actual length, still works correctly on the final partial chunk — the loop condition start < message.length() will already be false the next time around either way.
  • Calling fillBlock and encryptBlock exactly as given, instead of re-deriving their logic, is explicitly what the question asks for — it states that solutions which reimplement either method's functionality won't receive full credit.

Tracing the Example

Using the question's own example — letterBlock sized 2 rows by 3 columns (blockSize = 6) and encryptMessage("Meet at midnight") (length 16):

Chunk piece letterBlock after fillBlock encryptBlock() returns Running result
1st (start=0) "Meet a" row 0: "M" "e" "e", row 1: "t" " " "a" "Mte ea" "Mte ea"
2nd (start=6) "t midn" row 0: "t" " " "m", row 1: "i" "d" "n" "ti dmn" "Mte eati dmn"
3rd (start=12) "ight" (padded to "ightAA") row 0: "i" "g" "h", row 1: "t" "A" "A" "itgAhA" "Mte eati dmnitgAhA"

After the third chunk, start becomes 18, which is no longer less than message.length() (16), so the loop ends. The final returned string, "Mte eati dmnitgAhA", matches the question's own worked example exactly.

Common Mistakes to Avoid

  • Advancing start by the chunk's actual (possibly shorter) length instead of the full blockSize. It's usually harmless since it only affects the last iteration, but it conflates two different quantities and is easier to get wrong.
  • Using a do-while loop instead of a while loop, which would run once even for an empty message and fail the "return the empty string" requirement.
  • Overwriting result instead of appending to it (result = encryptBlock(); instead of result = result + encryptBlock();), which keeps only the last chunk's encrypted text.
  • Calling encryptBlock() before fillBlock() for a given chunk, which would encrypt stale data left over from the previous chunk instead of the current one.

Key Takeaways

  • Converting between a 2D row/column position and a single running index is one formula: row * numColumns + column.
  • Processing a long input in fixed-size chunks is a while loop pattern: take the next chunk (clamped to whatever remains), process it, advance by the full chunk size, and repeat.
  • When a problem says reimplementing a given method "will not receive full credit," that's telling you exactly which piece to treat as an already-solved building block rather than something to rewrite yourself.

Related FRQs