title: Alphabet Board Path
slug: alphabet-board-path
difficulty: medium
leetcode_id: 1138
leetcode_url: https://leetcode.com/problems/alphabet-board-path/
categories:
  - strings
  - hash-tables
patterns:
  - slug: matrix-traversal
    is_optimal: true

function_signature: "def alphabet_board_path(target: str) -> str:"

test_cases:
  visible:
    - input: { target: "leet" }
      expected: "DDR!UURRR!!DDD!"
    - input: { target: "code" }
      expected: "RR!DDRR!UUL!R!"
  hidden:
    - input: { target: "a" }
      expected: "!"
    - input: { target: "z" }
      expected: "DDDDD!"
    - input: { target: "zdz" }
      expected: "DDDDD!UUUUURRR!LLLDDDDD!"
    - input: { target: "aa" }
      expected: "!!"
    - input: { target: "abc" }
      expected: "!R!R!"

description: |
  On an alphabet board, we start at position `(0, 0)`, corresponding to character `board[0][0]`.

  The board is defined as:

  ```
  board = ["abcde", "fghij", "klmno", "pqrst", "uvwxy", "z"]
  ```

  We may make the following moves:

  - `'U'` moves our position up one row, if the position exists on the board
  - `'D'` moves our position down one row, if the position exists on the board
  - `'L'` moves our position left one column, if the position exists on the board
  - `'R'` moves our position right one column, if the position exists on the board
  - `'!'` adds the character at our current position to the answer

  Return a sequence of moves that makes our answer equal to `target` in the **minimum number of moves**. You may return any valid path.

constraints: |
  - `1 <= target.length <= 100`
  - `target` consists only of English lowercase letters

examples:
  - input: 'target = "leet"'
    output: '"DDR!UURRR!!DDD!"'
    explanation: "Starting at 'a' (0,0), move down twice and right once to reach 'l', press '!'. Then move up twice and right three times to reach 'e', press '!' twice (since 'e' appears twice). Finally move down three times to reach 't' and press '!'."
  - input: 'target = "code"'
    output: '"RR!DDRR!UUL!R!"'
    explanation: "Navigate from 'a' to 'c' (right twice), then to 'o', then to 'd', then to 'e', pressing '!' after reaching each character."

explanation:
  intuition: |
    Imagine the alphabet board as a **coordinate grid** where each letter has a fixed position. Since the letters are arranged sequentially (`a` at `(0,0)`, `b` at `(0,1)`, etc.), we can calculate any letter's position using simple arithmetic:

    - **Row** = `(char - 'a') // 5` (integer division by 5, since each row has 5 letters)
    - **Column** = `(char - 'a') % 5` (remainder when divided by 5)

    The problem becomes: given a current position and a target position, what's the shortest path between them? On a grid with no obstacles, this is simply moving the required number of steps in each direction.

    However, there's a **critical trap**: the letter `'z'` sits alone at position `(5, 0)`. The last row only has one cell! If you try to move right while on row 5, you'll fall off the board. Similarly, if you're at column > 0 and try to move down to row 5, you'll be stuck.

    The solution is to **control the order of moves**: when moving *toward* `'z'`, move left/up first before moving down. When moving *away from* `'z'`, move up first before moving right. This ensures we never land on an invalid position.

  approach: |
    We solve this using a **Coordinate Calculation with Ordered Moves** approach:

    **Step 1: Create a position lookup**

    - For each letter `'a'` to `'z'`, calculate its `(row, col)` position
    - Row = `(ord(char) - ord('a')) // 5`
    - Column = `(ord(char) - ord('a')) % 5`
    - This can be done on-the-fly or precomputed

    &nbsp;

    **Step 2: Initialise tracking variables**

    - `current_row`: Set to `0` (starting position)
    - `current_col`: Set to `0` (starting position)
    - `result`: Empty list to collect move characters

    &nbsp;

    **Step 3: Process each character in target**

    - Calculate the target position `(target_row, target_col)`
    - Determine the row and column differences:
      - `row_diff = target_row - current_row`
      - `col_diff = target_col - current_col`

    &nbsp;

    **Step 4: Generate moves in the correct order**

    - **Move up first** (if `row_diff < 0`): Add `'U'` × `|row_diff|` times
    - **Move left next** (if `col_diff < 0`): Add `'L'` × `|col_diff|` times
    - **Move down** (if `row_diff > 0`): Add `'D'` × `row_diff` times
    - **Move right last** (if `col_diff > 0`): Add `'R'` × `col_diff` times
    - This order (U → L → D → R) ensures we never get stuck at `'z'`

    &nbsp;

    **Step 5: Add the selection and update position**

    - Append `'!'` to select the current character
    - Update `current_row` and `current_col` to the target position

    &nbsp;

    **Step 6: Return the result**

    - Join all moves into a single string

  common_pitfalls:
    - title: The 'z' Trap
      description: |
        The most common bug is ignoring that `'z'` is the only letter in its row. Consider moving from `'z'` (5, 0) to `'e'` (0, 4):

        If you move right first: you try to go to `(5, 1)` — but that position doesn't exist! The board only has `'z'` at `(5, 0)`.

        The fix is to **always move up/left before down/right**. This ensures you leave row 5 before moving horizontally, and you move to column 0 before entering row 5.
      wrong_approach: "Move in any order (e.g., right before up)"
      correct_approach: "Always process moves in order: Up, Left, Down, Right"

    - title: Forgetting to Handle Staying in Place
      description: |
        If the same character appears consecutively in `target` (e.g., `"ee"`), you don't need any moves — just add `'!'`.

        Some implementations incorrectly add empty move strings or fail to handle zero differences. The solution handles this naturally since `'U' * 0` produces an empty string.

    - title: Off-by-One in Position Calculation
      description: |
        Remember that `ord('a')` is the baseline. The formula `(ord(char) - ord('a'))` gives values 0-25 for 'a'-'z'.

        - `'a'` → 0 → row 0, col 0
        - `'f'` → 5 → row 1, col 0
        - `'z'` → 25 → row 5, col 0

        Double-check your arithmetic, especially for edge characters like `'e'` (row 0, col 4) and `'z'` (row 5, col 0).

  key_takeaways:
    - "**Coordinate mapping**: Converting characters to grid positions using arithmetic (`// 5` for row, `% 5` for column) is a common technique for board problems"
    - "**Order matters**: When navigating grids with irregular shapes, the sequence of moves can determine validity — this pattern appears in many path-finding problems"
    - "**Handle edge cases explicitly**: The `'z'` special case is the crux of this problem; always analyse board boundaries carefully"
    - "**Greedy works here**: Since we're on a grid with no obstacles (except the shape constraint), the shortest path is simply the Manhattan distance — no need for BFS/DFS"

  time_complexity: "O(n × k) where `n` is the length of `target` and `k` is the average number of moves per character. Since the board is 6×5, `k` is at most 9 (5 rows + 4 columns), so this simplifies to **O(n)**."
  space_complexity: "O(n × k) for storing the result string. In the worst case, each character requires up to 9 moves plus '!', so the output can be up to 10× the input length. This simplifies to **O(n)**."

solutions:
  - approach_name: Coordinate Calculation with Ordered Moves
    is_optimal: true
    code: |
      def alphabet_board_path(target: str) -> str:
          result = []
          # Start at 'a' which is at position (0, 0)
          curr_row, curr_col = 0, 0

          for char in target:
              # Calculate target position from character
              # 'a' is 0, 'b' is 1, ..., 'z' is 25
              char_index = ord(char) - ord('a')
              target_row = char_index // 5  # 5 letters per row
              target_col = char_index % 5

              # Calculate how many steps in each direction
              row_diff = target_row - curr_row
              col_diff = target_col - curr_col

              # CRITICAL: Order matters to avoid falling off at 'z'
              # Move UP first (away from row 5)
              if row_diff < 0:
                  result.append('U' * (-row_diff))

              # Move LEFT next (toward column 0, needed before going to 'z')
              if col_diff < 0:
                  result.append('L' * (-col_diff))

              # Move DOWN (toward row 5, but only after we're at column 0)
              if row_diff > 0:
                  result.append('D' * row_diff)

              # Move RIGHT last (only valid after leaving row 5)
              if col_diff > 0:
                  result.append('R' * col_diff)

              # Select the character
              result.append('!')

              # Update current position
              curr_row, curr_col = target_row, target_col

          return ''.join(result)
    explanation: |
      **Time Complexity:** O(n) — We process each character once, and each character requires at most O(1) position calculations and O(k) string operations where k ≤ 9.

      **Space Complexity:** O(n) — The result string grows linearly with input length.

      The key insight is processing moves in a specific order (U → L → D → R) to handle the irregular board shape where 'z' is alone on the last row.

  - approach_name: Using Position Dictionary
    is_optimal: true
    code: |
      def alphabet_board_path(target: str) -> str:
          # Precompute positions for all letters
          positions = {}
          for i, char in enumerate("abcdefghijklmnopqrstuvwxyz"):
              positions[char] = (i // 5, i % 5)

          result = []
          curr_row, curr_col = 0, 0

          for char in target:
              target_row, target_col = positions[char]

              # Move up before moving right (escape from 'z' row)
              while curr_row > target_row:
                  result.append('U')
                  curr_row -= 1

              # Move left before moving down (prepare for 'z')
              while curr_col > target_col:
                  result.append('L')
                  curr_col -= 1

              # Move down after horizontal adjustment
              while curr_row < target_row:
                  result.append('D')
                  curr_row += 1

              # Move right after leaving 'z' row
              while curr_col < target_col:
                  result.append('R')
                  curr_col += 1

              result.append('!')

          return ''.join(result)
    explanation: |
      **Time Complexity:** O(n) — Same as the first approach, with O(26) preprocessing.

      **Space Complexity:** O(n) — For the result, plus O(26) = O(1) for the positions dictionary.

      This version uses a precomputed dictionary and while loops instead of string multiplication. Both approaches are equally optimal; this one may be slightly more readable for some.
