Facebook Pixel

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:

  1. Rotate the ring clockwise or counterclockwise to bring the desired character to the "12:00" position. Each position moved counts as one step.
  2. 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" and key = "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 and key. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 to j
  • 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 the key (which character we're spelling)
  • j represents the position in the ring (where we're currently aligned)
  • f[i][j] stores the minimum steps to spell the first i+1 characters with position j 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 where key[i] appears in the ring
  • All positions k where the previous character key[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 position k: f[i-1][k]
  • The rotation cost from position k to position j: 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 Evaluator

Example 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:

  1. Start at position 0 with 'g' → Press button (1 step)
  2. Rotate clockwise 2 positions to reach 'd' at index 2 → 2 rotations + 1 button press (3 steps)
  3. 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 takes O(n) time where n = len(ring)
  • The main DP computation has three nested loops:
    • Outer loop iterates through each character in key: O(m) where m = len(key)
    • Middle loop iterates through all positions of key[i] in the ring: worst case O(n) positions
    • Inner loop iterates through all positions of key[i-1] in the ring: worst case O(n) positions
  • 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 size m × n, requiring O(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]])
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More