514. Freedom Trail
Problem Description
You're given a circular dial (like a combination lock) represented by a string ring
, and you need to spell out a keyword represented by string key
. The dial starts with its first character at the "12:00" position.
To spell each character in key
, you need to:
- Rotate the ring clockwise or counterclockwise to bring the desired character to the "12:00" position. Each position moved counts as one step.
- Press the center button to confirm the character, which counts as one additional step.
Since the ring is circular, you can rotate in either direction to reach any character. For example, if the ring has length n
, moving from position i
to position j
takes either |i - j|
steps or n - |i - j|
steps, whichever is smaller.
The goal is to find the minimum total number of steps needed to spell all characters in key
in order.
Example walkthrough:
- If
ring = "godding"
andkey = "gd"
- Initially, 'g' (at index 0) is at "12:00", so spelling 'g' takes 0 rotations + 1 button press = 1 step
- Next, to spell 'd', we can rotate to index 2 (clockwise 2 steps) or index 6 (counterclockwise 1 step)
- Choosing index 6: 1 rotation + 1 button press = 2 steps
- Total: 1 + 2 = 3 steps
The solution uses dynamic programming where f[i][j]
represents the minimum steps to spell the first i+1
characters of key
with the j
-th character of ring
at the "12:00" position. The algorithm considers all possible positions for each character and chooses the path with minimum total steps.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: We can model this problem as a graph where each state represents a position in both the
ring
andkey
. Each character position in the ring can be viewed as a node, and moving between positions to spell the next character creates edges.
Is it a tree?
- No: The graph structure is not a tree because we can have multiple paths to reach the same state (same position in ring for the same character in key), and there can be cycles when rotating the ring.
Is the problem related to directed acyclic graphs?
- No: The state transition graph is not acyclic. We can rotate the ring in circles, and revisit positions.
Is the problem related to shortest paths?
- No: While we want the minimum steps, this is more of an optimization problem over states rather than finding shortest paths between nodes in a traditional sense.
Does the problem involve connectivity?
- No: We're not checking if components are connected or finding connected components.
Does the problem have small constraints?
- Yes: The constraints are typically small (ring and key lengths are usually limited to around 100 characters), making it feasible to explore all possible state transitions.
DFS/backtracking?
- Yes: We can use DFS with memoization (dynamic programming) to explore all possible ways to spell the key by trying different positions for each character and backtracking to find the optimal solution.
Conclusion: The flowchart suggests using a DFS/backtracking approach. In this problem, we use DFS with dynamic programming where we explore all possible positions to align each character of the key, keeping track of the minimum steps needed. The recursive nature of trying different paths and selecting the optimal one aligns perfectly with the DFS pattern.
Intuition
The key insight is recognizing that at each step, we face a decision problem: which occurrence of the next character should we rotate to? Since the ring is circular and characters can repeat, we have multiple choices for each character we need to spell.
Think of it like planning a route through multiple cities where some city names repeat. At each step, you need to decide which instance of the next city to visit, considering both the distance from your current position and the future destinations you'll need to reach.
This naturally leads to a state-based optimization problem. Our state can be defined as: "I've spelled the first i
characters of the key, and I'm currently at position j
on the ring." From each state, we can transition to any position where the next required character appears.
The recursive structure becomes clear:
- To find the minimum steps to spell all characters ending at position
j
- We need to know the minimum steps to spell all previous characters ending at some position
k
- Plus the cost to rotate from
k
toj
- Plus 1 for pressing the button
Since the same subproblems (spelling the first i
characters and ending at position j
) will be encountered multiple times through different paths, we can use dynamic programming to avoid recalculating them.
The DP approach emerges naturally: f[i][j]
represents the minimum steps to spell the first i+1
characters with position j
aligned at 12:00. We build up the solution by considering all possible ways to reach each state and keeping only the minimum cost path. This is essentially DFS with memoization, where we explore all paths but remember the best solution for each state we've visited.
Learn more about Depth-First Search, Breadth-First Search and Dynamic Programming patterns.
Solution Approach
The implementation uses dynamic programming with the following components:
1. Preprocessing Character Positions
First, we create a dictionary pos
that maps each character to its list of positions in the ring:
pos = defaultdict(list)
for i, c in enumerate(ring):
pos[c].append(i)
This allows us to quickly find all occurrences of any character we need to spell.
2. DP Table Initialization
We create a 2D DP table f[i][j]
where:
i
represents the index in thekey
(which character we're spelling)j
represents the position in thering
(where we're currently aligned)f[i][j]
stores the minimum steps to spell the firsti+1
characters with positionj
at 12:00
f = [[inf] * n for _ in range(m)]
3. Base Case
For the first character of the key, we calculate the cost to rotate to each occurrence of that character from position 0:
for j in pos[key[0]]:
f[0][j] = min(j, n - j) + 1
The rotation cost is min(j, n - j)
because we can rotate either clockwise or counterclockwise. We add 1 for the button press.
4. State Transitions
For each subsequent character key[i]
, we consider:
- All positions
j
wherekey[i]
appears in the ring - All positions
k
where the previous characterkey[i-1]
appeared
The transition formula is:
f[i][j] = min(f[i][j], f[i-1][k] + min(abs(j - k), n - abs(j - k)) + 1)
This calculates:
- The cost to spell the first
i
characters ending at positionk
:f[i-1][k]
- The rotation cost from position
k
to positionj
:min(abs(j - k), n - abs(j - k))
- Plus 1 for pressing the button
5. Finding the Answer
The final answer is the minimum value among all possible ending positions for the last character:
return min(f[-1][j] for j in pos[key[-1]])
Time Complexity: O(m × n²)
where m
is the length of the key and n
is the length of the ring. In the worst case, each character appears at every position in the ring.
Space Complexity: O(m × n)
for the DP table plus O(n)
for the position dictionary.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example with ring = "godding"
and key = "gd"
:
Initial Setup:
- Ring: "godding" (length n = 7)
- Key: "gd" (length m = 2)
- Starting position: index 0 ('g' is at 12:00)
Step 1: Preprocess character positions
pos = { 'g': [0, 6], # 'g' appears at indices 0 and 6 'o': [1], # 'o' appears at index 1 'd': [2, 3], # 'd' appears at indices 2 and 3 'i': [4], # 'i' appears at index 4 'n': [5] # 'n' appears at index 5 }
Step 2: Initialize DP table
f = [[inf, inf, inf, inf, inf, inf, inf], [inf, inf, inf, inf, inf, inf, inf]]
Step 3: Base case - spelling first character 'g'
Since 'g' appears at positions [0, 6]:
- Position 0: Rotation cost = min(0, 7-0) = 0, Total = 0 + 1 = 1
- Position 6: Rotation cost = min(6, 7-6) = 1, Total = 1 + 1 = 2
f[0] = [1, inf, inf, inf, inf, inf, 2]
Step 4: Spell second character 'd'
'd' appears at positions [2, 3]. We check transitions from previous 'g' positions:
For position j=2 (spelling 'd' at index 2):
- From k=0: f[0][0] + rotation(0→2) + 1 = 1 + min(2, 7-2) + 1 = 1 + 2 + 1 = 4
- From k=6: f[0][6] + rotation(6→2) + 1 = 2 + min(4, 7-4) + 1 = 2 + 3 + 1 = 6
- Choose minimum: f[1][2] = 4
For position j=3 (spelling 'd' at index 3):
- From k=0: f[0][0] + rotation(0→3) + 1 = 1 + min(3, 7-3) + 1 = 1 + 3 + 1 = 5
- From k=6: f[0][6] + rotation(6→3) + 1 = 2 + min(3, 7-3) + 1 = 2 + 3 + 1 = 6
- Choose minimum: f[1][3] = 5
f[1] = [inf, inf, 4, 5, inf, inf, inf]
Step 5: Find the answer
The minimum among all valid ending positions for 'd':
- min(f[1][2], f[1][3]) = min(4, 5) = 4
Final answer: 4 steps
The optimal path is:
- Start at position 0 with 'g' → Press button (1 step)
- Rotate clockwise 2 positions to reach 'd' at index 2 → 2 rotations + 1 button press (3 steps)
- Total: 1 + 3 = 4 steps
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def findRotateSteps(self, ring: str, key: str) -> int:
6 key_length, ring_length = len(key), len(ring)
7
8 # Store all positions of each character in the ring
9 char_positions = defaultdict(list)
10 for index, char in enumerate(ring):
11 char_positions[char].append(index)
12
13 # dp[i][j] = minimum steps to spell key[0..i] with ring[j] at 12:00
14 # Initialize with infinity for all states
15 dp = [[float('inf')] * ring_length for _ in range(key_length)]
16
17 # Base case: spell the first character of key
18 # For each position where key[0] appears in ring
19 for position in char_positions[key[0]]:
20 # Calculate minimum rotation steps (clockwise or counter-clockwise)
21 # Plus 1 for pressing the button
22 dp[0][position] = min(position, ring_length - position) + 1
23
24 # Fill dp table for remaining characters in key
25 for key_index in range(1, key_length):
26 # For each position where current key character appears in ring
27 for current_pos in char_positions[key[key_index]]:
28 # Try all possible previous positions
29 for prev_pos in char_positions[key[key_index - 1]]:
30 # Calculate rotation distance between positions
31 distance = abs(current_pos - prev_pos)
32 # Choose minimum between clockwise and counter-clockwise rotation
33 min_rotation = min(distance, ring_length - distance)
34 # Update dp with minimum cost
35 dp[key_index][current_pos] = min(
36 dp[key_index][current_pos],
37 dp[key_index - 1][prev_pos] + min_rotation + 1
38 )
39
40 # Return minimum steps among all possible final positions
41 return min(dp[-1][position] for position in char_positions[key[-1]])
42
1class Solution {
2 public int findRotateSteps(String ring, String key) {
3 int keyLength = key.length();
4 int ringLength = ring.length();
5
6 // Create a position map for each character in the ring
7 // positions[i] contains all indices where character ('a' + i) appears in ring
8 List<Integer>[] positions = new List[26];
9 Arrays.setAll(positions, k -> new ArrayList<>());
10
11 // Populate the position map with indices for each character
12 for (int i = 0; i < ringLength; ++i) {
13 int charIndex = ring.charAt(i) - 'a';
14 positions[charIndex].add(i);
15 }
16
17 // dp[i][j] represents minimum steps to spell key[0...i] with ring position j
18 int[][] dp = new int[keyLength][ringLength];
19
20 // Initialize dp array with a large value (representing infinity)
21 for (int[] row : dp) {
22 Arrays.fill(row, 1 << 30);
23 }
24
25 // Base case: calculate minimum steps for the first character of key
26 // For each position of the first character in ring
27 for (int ringPos : positions[key.charAt(0) - 'a']) {
28 // Calculate minimum rotation: clockwise or counter-clockwise
29 int minRotation = Math.min(ringPos, ringLength - ringPos);
30 // Add 1 for the button press
31 dp[0][ringPos] = minRotation + 1;
32 }
33
34 // Fill dp table for remaining characters in key
35 for (int keyIndex = 1; keyIndex < keyLength; ++keyIndex) {
36 // For each position of current character in ring
37 for (int currentRingPos : positions[key.charAt(keyIndex) - 'a']) {
38 // Check all positions of previous character in ring
39 for (int prevRingPos : positions[key.charAt(keyIndex - 1) - 'a']) {
40 // Calculate distance between current and previous positions
41 int distance = Math.abs(currentRingPos - prevRingPos);
42 // Choose minimum between clockwise and counter-clockwise rotation
43 int minRotation = Math.min(distance, ringLength - distance);
44 // Update dp with minimum steps: previous steps + rotation + button press
45 dp[keyIndex][currentRingPos] = Math.min(
46 dp[keyIndex][currentRingPos],
47 dp[keyIndex - 1][prevRingPos] + minRotation + 1
48 );
49 }
50 }
51 }
52
53 // Find the minimum steps among all possible final positions
54 int minSteps = 1 << 30;
55 for (int finalRingPos : positions[key.charAt(keyLength - 1) - 'a']) {
56 minSteps = Math.min(minSteps, dp[keyLength - 1][finalRingPos]);
57 }
58
59 return minSteps;
60 }
61}
62
1class Solution {
2public:
3 int findRotateSteps(string ring, string key) {
4 int keyLength = key.size();
5 int ringLength = ring.size();
6
7 // Store all positions for each character in the ring
8 // charPositions[i] contains all positions where character ('a' + i) appears in ring
9 vector<int> charPositions[26];
10 for (int pos = 0; pos < ringLength; ++pos) {
11 charPositions[ring[pos] - 'a'].push_back(pos);
12 }
13
14 // dp[i][j] represents minimum steps to spell key[0...i] with ring position j
15 int dp[keyLength][ringLength];
16 memset(dp, 0x3f, sizeof(dp)); // Initialize with large value (infinity)
17
18 // Base case: first character of key
19 // Calculate minimum steps to reach each position of first character from position 0
20 for (int ringPos : charPositions[key[0] - 'a']) {
21 // Can rotate clockwise (ringPos steps) or counter-clockwise (ringLength - ringPos steps)
22 dp[0][ringPos] = min(ringPos, ringLength - ringPos) + 1; // +1 for button press
23 }
24
25 // Fill dp table for remaining characters in key
26 for (int keyIdx = 1; keyIdx < keyLength; ++keyIdx) {
27 // For each position where current key character appears in ring
28 for (int currPos : charPositions[key[keyIdx] - 'a']) {
29 // Try all positions of previous character
30 for (int prevPos : charPositions[key[keyIdx - 1] - 'a']) {
31 // Calculate rotation distance between positions
32 int clockwiseDist = abs(currPos - prevPos);
33 int counterClockwiseDist = ringLength - clockwiseDist;
34 int minRotation = min(clockwiseDist, counterClockwiseDist);
35
36 // Update dp with minimum steps
37 dp[keyIdx][currPos] = min(dp[keyIdx][currPos],
38 dp[keyIdx - 1][prevPos] + minRotation + 1); // +1 for button press
39 }
40 }
41 }
42
43 // Find minimum among all possible ending positions for last character
44 int minSteps = INT_MAX;
45 for (int finalPos : charPositions[key[keyLength - 1] - 'a']) {
46 minSteps = min(minSteps, dp[keyLength - 1][finalPos]);
47 }
48
49 return minSteps;
50 }
51};
52
1/**
2 * Finds the minimum number of steps to spell out a key string by rotating a ring
3 * @param ring - The ring string that can be rotated clockwise or anticlockwise
4 * @param key - The target string to spell out
5 * @returns The minimum number of steps (rotations + button presses) needed
6 */
7function findRotateSteps(ring: string, key: string): number {
8 const keyLength: number = key.length;
9 const ringLength: number = ring.length;
10
11 // Store all positions for each character in the ring
12 // positions[i] contains all indices where character (i + 'a') appears in the ring
13 const positions: number[][] = Array.from({ length: 26 }, () => []);
14
15 // Populate the positions array with indices for each character
16 for (let i = 0; i < ringLength; ++i) {
17 const charIndex: number = ring.charCodeAt(i) - 'a'.charCodeAt(0);
18 positions[charIndex].push(i);
19 }
20
21 // Dynamic programming table
22 // dp[i][j] represents minimum steps to spell key[0...i] with ring position j
23 const dp: number[][] = Array.from(
24 { length: keyLength },
25 () => Array(ringLength).fill(1 << 30) // Initialize with large value
26 );
27
28 // Base case: calculate steps for the first character of key
29 // Starting from position 0, we can rotate clockwise or anticlockwise
30 const firstCharPositions = positions[key.charCodeAt(0) - 'a'.charCodeAt(0)];
31 for (const targetPosition of firstCharPositions) {
32 // Minimum distance is either clockwise or anticlockwise rotation
33 const clockwiseDistance: number = targetPosition;
34 const anticlockwiseDistance: number = ringLength - targetPosition;
35 dp[0][targetPosition] = Math.min(clockwiseDistance, anticlockwiseDistance) + 1; // +1 for button press
36 }
37
38 // Fill the DP table for remaining characters
39 for (let i = 1; i < keyLength; ++i) {
40 const currentCharPositions = positions[key.charCodeAt(i) - 'a'.charCodeAt(0)];
41 const previousCharPositions = positions[key.charCodeAt(i - 1) - 'a'.charCodeAt(0)];
42
43 // For each position where current character appears
44 for (const currentPosition of currentCharPositions) {
45 // Check all possible previous positions
46 for (const previousPosition of previousCharPositions) {
47 // Calculate minimum rotation distance between positions
48 const directDistance: number = Math.abs(currentPosition - previousPosition);
49 const aroundDistance: number = ringLength - directDistance;
50 const minRotation: number = Math.min(directDistance, aroundDistance);
51
52 // Update DP value: previous cost + rotation cost + button press
53 dp[i][currentPosition] = Math.min(
54 dp[i][currentPosition],
55 dp[i - 1][previousPosition] + minRotation + 1
56 );
57 }
58 }
59 }
60
61 // Find the minimum cost among all possible final positions
62 let minSteps: number = 1 << 30; // Initialize with large value
63 const lastCharPositions = positions[key.charCodeAt(keyLength - 1) - 'a'.charCodeAt(0)];
64 for (const finalPosition of lastCharPositions) {
65 minSteps = Math.min(minSteps, dp[keyLength - 1][finalPosition]);
66 }
67
68 return minSteps;
69}
70
Time and Space Complexity
Time Complexity: O(m × n²)
The algorithm uses dynamic programming with the following time analysis:
- Building the position dictionary
pos
takesO(n)
time wheren = len(ring)
- The main DP computation has three nested loops:
- Outer loop iterates through each character in
key
:O(m)
wherem = len(key)
- Middle loop iterates through all positions of
key[i]
in the ring: worst caseO(n)
positions - Inner loop iterates through all positions of
key[i-1]
in the ring: worst caseO(n)
positions
- Outer loop iterates through each character in
- The final minimum calculation takes
O(n)
in the worst case
The dominant operation is the triple nested loop structure, giving us O(m × n × n) = O(m × n²)
Space Complexity: O(m × n)
The space usage consists of:
- The position dictionary
pos
:O(n)
space to store all character positions - The DP table
f
: a 2D array of sizem × n
, requiringO(m × n)
space - Other variables use constant space
The DP table dominates the space complexity, resulting in O(m × n)
total space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting the Circular Nature of the Ring
A common mistake is calculating the rotation distance as simply abs(j - k)
without considering that the ring is circular. This leads to suboptimal or incorrect results.
Incorrect approach:
# Wrong: Only considers linear distance
rotation_cost = abs(current_pos - prev_pos)
Correct approach:
# Consider both clockwise and counterclockwise paths
distance = abs(current_pos - prev_pos)
rotation_cost = min(distance, ring_length - distance)
2. Off-by-One Error with Button Presses
Many implementations forget to add the button press step for each character, or accidentally add it multiple times in the calculation.
Incorrect approach:
# Forgetting the button press
dp[i][j] = dp[i-1][k] + min_rotation # Missing +1
# Or double-counting in base case
dp[0][position] = min(position, ring_length - position) + 1
# Then later also adding +1 in transitions
Correct approach:
# Consistently add exactly one button press per character dp[i][j] = dp[i-1][k] + min_rotation + 1 # Always include the +1
3. Improper Base Case Initialization
Starting from an incorrect initial position or not properly handling the first character's rotation from position 0.
Incorrect approach:
# Wrong: Assumes we can start from any position for j in char_positions[key[0]]: dp[0][j] = 1 # Ignores rotation cost from initial position
Correct approach:
# Account for rotation from starting position (index 0)
for j in char_positions[key[0]]:
dp[0][j] = min(j, ring_length - j) + 1
4. Not Handling Duplicate Characters Properly
When a character appears multiple times in the ring, failing to consider all occurrences can lead to missing the optimal solution.
Incorrect approach:
# Only considering the first occurrence position = ring.index(key[i]) # Gets only the first occurrence
Correct approach:
# Consider all positions where the character appears for position in char_positions[key[i]]: # Process each possible position
5. Memory Optimization Oversight
While the current solution works, it uses O(m × n) space. For very large inputs, this could be optimized to O(n) since we only need the previous row of the DP table.
Space-optimized approach:
def findRotateSteps(self, ring: str, key: str) -> int:
n = len(ring)
char_positions = defaultdict(list)
for i, c in enumerate(ring):
char_positions[c].append(i)
# Use only two rows instead of full table
prev = [float('inf')] * n
curr = [float('inf')] * n
# Base case
for j in char_positions[key[0]]:
prev[j] = min(j, n - j) + 1
# Process remaining characters
for i in range(1, len(key)):
curr = [float('inf')] * n
for j in char_positions[key[i]]:
for k in char_positions[key[i-1]]:
dist = abs(j - k)
curr[j] = min(curr[j], prev[k] + min(dist, n - dist) + 1)
prev = curr
return min(prev[j] for j in char_positions[key[-1]])
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!