LeetCode 514 - Freedom Trail
The problem models a circular dial, represented by the string ring, where each character is engraved at a position around the circle. Another string, key, represents the sequence of characters we must spell.
Difficulty: 🔴 Hard
Topics: String, Dynamic Programming, Depth-First Search, Breadth-First Search
Solution
LeetCode 514 - Freedom Trail
Problem Understanding
The problem models a circular dial, represented by the string ring, where each character is engraved at a position around the circle. Another string, key, represents the sequence of characters we must spell.
At the beginning, the character at index 0 of ring is aligned at the "12:00" position. To spell each character in key, we may rotate the ring clockwise or counterclockwise until one occurrence of the required character is aligned at "12:00". After alignment, we must press the center button, which costs one additional step.
The goal is to compute the minimum total number of steps required to spell the entire key.
The important detail is that the ring is circular. Moving from position 0 to position n - 1 costs only one step because we can rotate in either direction. For any two positions, the actual rotation cost is the minimum of:
- clockwise distance
- counterclockwise distance
The input sizes are relatively small:
1 <= ring.length <= 1001 <= key.length <= 100
Even though these limits are not huge, a naive recursive exploration can still become extremely expensive because many characters may appear multiple times in the ring. Every duplicate character creates branching choices.
Another important observation is that the problem guarantees every character in key exists somewhere in ring. Therefore, we never need to handle impossible states.
Several edge cases are important:
- A character may appear many times in
ring, and choosing the closest occurrence greedily is not always optimal. - The ring is circular, so wrapping around must be handled correctly.
ringandkeycan both have length1.- Consecutive identical characters in
keyshould only require pressing the button again without rotating.
Approaches
Brute Force Approach
A brute force solution recursively tries every possible occurrence of each character in key.
Suppose the current ring pointer is at position current, and we need to spell key[i]. We can:
- Find every index in
ringwherering[index] == key[i] - Rotate to each candidate index
- Recursively solve the remaining suffix of
key
The recursive transition computes:
- rotation cost
- plus one button press
- plus the cost of solving the remaining characters
This approach is correct because it explores every possible sequence of rotations and therefore eventually finds the minimum total cost.
However, it is too slow without memoization. If a character appears multiple times, the recursion branches heavily. In the worst case, the number of states grows exponentially with the length of key.
Key Insight
The crucial observation is that the problem contains overlapping subproblems.
The future cost depends only on:
- the current position of the ring
- the current index in
key
If we already computed the minimum cost for a state (ring_position, key_index), we should reuse it instead of recomputing it.
This naturally leads to dynamic programming.
We also preprocess the ring by storing all indices for each character. Then, instead of scanning the entire ring every time, we can directly jump to all candidate positions for a target character.
The optimal solution uses DFS with memoization:
-
State:
(current_ring_position, current_key_index) -
Transition:
-
try every matching occurrence of
key[current_key_index] -
compute rotation distance
-
recursively solve the remaining substring
Because there are at most 100 * 100 = 10,000 states, the solution becomes efficient.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | O(key.length) | Explores every possible rotation sequence |
| Optimal DFS + Memoization | O(K × R × M) | O(K × R) | Reuses subproblem results and only explores valid character positions |
Where:
K = len(key)R = len(ring)M = average number of matching character positions
Since all constraints are at most 100, this runs comfortably within limits.
Algorithm Walkthrough
- Create a mapping from each character to all indices where it appears in
ring.
This preprocessing step is important because many characters may appear multiple times. Instead of scanning the ring repeatedly, we can instantly retrieve all candidate positions for a character. 2. Define a recursive DFS function.
The DFS state is:
ring_pos, the current aligned positionkey_index, the next character we need to spell
The function returns the minimum number of steps needed to spell key[key_index:].
3. Handle the base case.
If key_index == len(key), then we already spelled the entire key, so the remaining cost is 0.
4. Retrieve all candidate positions for the target character.
Let:
target = key[key_index]
Using the preprocessing map, obtain every position in ring where this character appears.
5. Compute rotation cost for each candidate.
For every candidate position:
- clockwise distance:
abs(candidate - ring_pos)
- counterclockwise distance:
ring_length - abs(candidate - ring_pos)
The actual rotation cost is the smaller of the two. 6. Recursively compute the remaining cost.
The total cost becomes:
- rotation steps
- plus one button press
- plus recursive cost for the next character
- Take the minimum over all candidates.
Since multiple occurrences may exist, we must explore all of them to ensure optimality. 8. Memoize the result.
Store the answer for (ring_pos, key_index) so repeated states are computed only once.
Why it works
The algorithm works because every valid solution can be decomposed into smaller independent subproblems. At each stage, the only information needed for future decisions is:
- the current ring alignment
- the next key character index
The DFS explores every valid next alignment while memoization guarantees each state is solved once. Since the recurrence always considers all possible transitions and takes the minimum, the final result is globally optimal.
Python Solution
from collections import defaultdict
from functools import lru_cache
from typing import List
class Solution:
def findRotateSteps(self, ring: str, key: str) -> int:
char_positions = defaultdict(list)
for index, char in enumerate(ring):
char_positions[char].append(index)
ring_length = len(ring)
@lru_cache(None)
def dfs(ring_pos: int, key_index: int) -> int:
if key_index == len(key):
return 0
target_char = key[key_index]
minimum_steps = float("inf")
for target_pos in char_positions[target_char]:
direct_distance = abs(target_pos - ring_pos)
rotation_steps = min(
direct_distance,
ring_length - direct_distance
)
total_steps = (
rotation_steps
+ 1
+ dfs(target_pos, key_index + 1)
)
minimum_steps = min(minimum_steps, total_steps)
return minimum_steps
return dfs(0, 0)
The implementation begins by preprocessing the ring into a dictionary that maps each character to all of its indices. This allows constant time access to all candidate positions for a target character.
The recursive dfs function represents the dynamic programming state. Its parameters uniquely define the current problem:
ring_posis the currently aligned positionkey_indexindicates which character inkeymust be spelled next
The @lru_cache decorator memoizes results automatically. Without memoization, many identical states would be recomputed repeatedly.
Inside the DFS, we iterate through every occurrence of the target character. For each candidate:
- compute the circular rotation distance
- add one button press
- recursively solve the remaining suffix
Finally, we return the smallest total cost among all candidates.
Go Solution
package main
import "math"
func findRotateSteps(ring string, key string) int {
charPositions := make(map[byte][]int)
for i := 0; i < len(ring); i++ {
charPositions[ring[i]] = append(charPositions[ring[i]], i)
}
ringLength := len(ring)
type State struct {
ringPos int
keyIndex int
}
memo := make(map[State]int)
var dfs func(int, int) int
dfs = func(ringPos int, keyIndex int) int {
if keyIndex == len(key) {
return 0
}
state := State{ringPos, keyIndex}
if value, exists := memo[state]; exists {
return value
}
minimumSteps := math.MaxInt32
targetChar := key[keyIndex]
for _, targetPos := range charPositions[targetChar] {
directDistance := abs(targetPos - ringPos)
rotationSteps := min(
directDistance,
ringLength-directDistance,
)
totalSteps := rotationSteps +
1 +
dfs(targetPos, keyIndex+1)
minimumSteps = min(minimumSteps, totalSteps)
}
memo[state] = minimumSteps
return minimumSteps
}
return dfs(0, 0)
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
The Go implementation follows the same logic as the Python solution, but Go does not provide built in memoization decorators like lru_cache. Instead, we explicitly store results in a map[State]int.
A custom State struct is used as the memoization key because the DP state depends on both:
- current ring position
- current key index
Go also requires helper functions for abs and min.
Since all values are small, integer overflow is not a concern.
Worked Examples
Example 1
Input:
ring = "godding"
key = "gd"
Preprocessing
| Character | Positions |
|---|---|
| g | [0, 6] |
| o | [1] |
| d | [2, 3] |
| i | [4] |
| n | [5] |
Ring length is 7.
Initial state:
ring_pos = 0
key_index = 0
target = 'g'
Step 1, Spell 'g'
Possible positions for 'g':
| Target Position | Distance | Circular Cost |
|---|---|---|
| 0 | 0 | 0 |
| 6 | 6 | 1 |
Option 1: Use position 0
rotation = 0
press = 1
Current cost:
1
Recursive state:
dfs(0, 1)
Step 2, Spell 'd'
Possible positions for 'd':
| Target Position | Distance | Circular Cost |
|---|---|---|
| 2 | 2 | 2 |
| 3 | 3 | 3 |
Choose position 2.
Cost:
rotation = 2
press = 1
Total:
1 + 2 + 1 = 4
Final answer:
4
Example 2
Input:
ring = "godding"
key = "godding"
The algorithm repeatedly chooses optimal rotations while memoizing intermediate states.
A condensed trace:
| Key Character | Best Target Position | Rotation Cost | Press Cost | Running Total |
|---|---|---|---|---|
| g | 0 | 0 | 1 | 1 |
| o | 1 | 1 | 1 | 3 |
| d | 2 | 1 | 1 | 5 |
| d | 2 | 0 | 1 | 6 |
| i | 4 | 2 | 1 | 9 |
| n | 5 | 1 | 1 | 11 |
| g | 6 | 1 | 1 | 13 |
Final answer:
13
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(K × R × M) | Each DP state explores matching positions for a character |
| Space | O(K × R) | Memoization table stores one value per state |
Where:
K = len(key)R = len(ring)M = average occurrences of a character
In the worst case, every character appears everywhere, giving roughly O(K × R²). With maximum size 100, this is still efficient.
The space complexity comes from:
- the memoization cache
- the character position mapping
- recursion stack depth up to
len(key)
Test Cases
solution = Solution()
# Provided examples
assert solution.findRotateSteps("godding", "gd") == 4 # Basic example
assert solution.findRotateSteps("godding", "godding") == 13 # Full traversal
# Single character ring and key
assert solution.findRotateSteps("a", "a") == 1 # Only button press needed
# Repeated same character in key
assert solution.findRotateSteps("abcde", "aaa") == 3 # No extra rotation after first
# Multiple duplicate characters
assert solution.findRotateSteps("ababcab", "acba") == 8 # Requires choosing optimal duplicates
# Wraparound rotation
assert solution.findRotateSteps("abcde", "ea") == 4 # Circular movement is cheaper
# Ring with all identical characters
assert solution.findRotateSteps("aaaaa", "aaa") == 3 # Always already aligned
# Frequent branching possibilities
assert solution.findRotateSteps("pqwcx", "cpqwx") == 13 # Multiple directional choices
# Consecutive duplicate letters
assert solution.findRotateSteps("abcabc", "cc") == 2 # Stay on same aligned character
# Longer repeated pattern
assert solution.findRotateSteps("iotfo", "fioot") == 11 # Tests DP reuse
# Minimum boundary
assert solution.findRotateSteps("z", "z") == 1 # Smallest valid input
Test Summary
| Test | Why |
|---|---|
"godding", "gd" |
Verifies sample behavior |
"godding", "godding" |
Verifies longer traversal |
"a", "a" |
Smallest possible input |
"abcde", "aaa" |
Consecutive repeated key characters |
"ababcab", "acba" |
Multiple duplicate choices |
"abcde", "ea" |
Validates circular wraparound |
"aaaaa", "aaa" |
All characters identical |
"pqwcx", "cpqwx" |
Stress test for branching |
"abcabc", "cc" |
No movement between repeated characters |
"iotfo", "fioot" |
Ensures memoization effectiveness |
"z", "z" |
Boundary value test |
Edge Cases
Multiple Occurrences of the Same Character
A naive greedy strategy might always choose the nearest occurrence of a character. This can fail because a slightly more expensive move now may create a much cheaper future configuration.
For example, if several copies of a character exist around the ring, choosing the closest one may place the pointer badly for the next required letter. The implementation handles this correctly by exploring every occurrence and taking the global minimum through dynamic programming.
Circular Wraparound
The ring is circular, so moving from index 0 to index n - 1 costs only one step. Forgetting this detail leads to incorrect distance calculations.
The implementation correctly computes:
min(distance, ring_length - distance)
This guarantees the shorter rotational direction is always used.
Consecutive Identical Characters in the Key
If the next required character is already aligned at "12:00", no rotation is necessary. Only the button press should be counted.
For example:
ring = "abc"
key = "aa"
After spelling the first 'a', the second 'a' requires only another button press.
The DFS naturally handles this because the rotation distance becomes 0.
Ring with All Identical Characters
If every character in the ring is the same, there are many equivalent positions. A poorly designed algorithm may repeatedly recompute identical states.
Memoization prevents this inefficiency by caching previously solved states.