Facebook Pixel

3361. Shift Distance Between Two Strings

Problem Description

You have two strings s and t of equal length, consisting of lowercase English letters. Your goal is to transform string s into string t by changing individual characters, with each change having an associated cost.

For each character in s, you can perform one of two operations:

  • Shift forward: Move the character to the next letter in the alphabet (a→b, b→c, ..., z→a). The cost for this operation is nextCost[j], where j is the alphabet position of the current character (0 for 'a', 1 for 'b', ..., 25 for 'z').
  • Shift backward: Move the character to the previous letter in the alphabet (b→a, c→b, ..., a→z). The cost for this operation is previousCost[j], where j is the alphabet position of the current character.

The alphabet wraps around, meaning 'z' shifts forward to 'a', and 'a' shifts backward to 'z'.

To transform a character from s[i] to t[i], you may need multiple shifts. For example, to change 'a' to 'c', you could either:

  • Shift forward twice: 'a'→'b' (cost: nextCost[0]), then 'b'→'c' (cost: nextCost[1])
  • Shift backward 24 times: 'a'→'z'→'y'→...→'c'

The shift distance is the minimum total cost required to transform all characters in s to match the corresponding characters in t.

Given strings s and t, along with arrays nextCost and previousCost (each of length 26), return the minimum total cost to transform s into t.

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

Intuition

For each character position, we need to find the minimum cost to transform s[i] to t[i]. Since the alphabet is circular (wraps around), we have two possible paths to reach any target character from a source character:

  1. Moving forward (clockwise) through the alphabet
  2. Moving backward (counter-clockwise) through the alphabet

The key insight is that we need to efficiently calculate the cost of moving from any character to any other character in both directions. Since we might need to wrap around the alphabet, a naive approach of summing costs step by step would be inefficient.

To solve this efficiently, we can use prefix sums. The idea is to precompute cumulative costs for moving forward and backward through the alphabet. However, since the alphabet wraps around, we need to handle the circular nature carefully.

The solution doubles the alphabet conceptually to handle wrap-around cases. By creating prefix sum arrays of size 2 * 26, we can represent any path between two characters without worrying about the circular boundary. For example:

  • If we need to go from 'y' (position 24) to 'b' (position 1) forward, we can think of it as going to position 1 + 26 = 27 in the extended array
  • This allows us to use simple subtraction of prefix sums to get the total cost

For forward movement, we build prefix sums using nextCost, repeating the pattern for two cycles. For backward movement, we build prefix sums using previousCost.

Once we have these prefix sum arrays, for each character pair (s[i], t[i]), we:

  1. Calculate the forward path cost using the forward prefix sum array
  2. Calculate the backward path cost using the backward prefix sum array
  3. Take the minimum of these two costs

The total answer is the sum of minimum costs for all character positions.

Learn more about Prefix Sum patterns.

Solution Approach

The implementation uses prefix sums to efficiently calculate the minimum cost of transforming each character.

Step 1: Initialize Prefix Sum Arrays

We create two prefix sum arrays s1 and s2, each of size (26 << 1 | 1) which equals 53. This size allows us to handle the circular nature of the alphabet by essentially doubling it.

m = 26
s1 = [0] * (m << 1 | 1)  # For forward costs (size 53)
s2 = [0] * (m << 1 | 1)  # For backward costs (size 53)

Step 2: Build Prefix Sums

We populate the prefix sum arrays by iterating through two full cycles of the alphabet (52 iterations):

for i in range(m << 1):  # i goes from 0 to 51
    s1[i + 1] = s1[i] + nextCost[i % m]
    s2[i + 1] = s2[i] + previousCost[(i + 1) % m]
  • For s1: We accumulate nextCost[i % 26] to handle forward movement costs. The modulo operation ensures we cycle through the 26 costs twice.
  • For s2: We accumulate previousCost[(i + 1) % 26] for backward movement. The (i + 1) % m indexing accounts for the fact that moving backward from position i uses the cost at position i.

Step 3: Calculate Minimum Cost for Each Character

For each character pair (a, b) from strings s and t:

for a, b in zip(s, t):
    x, y = ord(a) - ord("a"), ord(b) - ord("a")

We convert characters to their alphabet positions (0-25).

Step 4: Compute Forward and Backward Costs

c1 = s1[y + m if y < x else y] - s1[x]
c2 = s2[x + m if x < y else x] - s2[y]
  • Forward cost (c1): If y < x (target comes before source in alphabet), we need to wrap around, so we use y + 26 as the endpoint. Otherwise, we use y directly. The cost is s1[endpoint] - s1[x].

  • Backward cost (c2): If x < y (source comes before target), we need to wrap around backward, so we use x + 26 as the starting point. The cost is s2[starting_point] - s2[y].

Step 5: Accumulate Minimum Costs

ans += min(c1, c2)

We take the minimum of forward and backward costs for each character position and sum them up.

The algorithm runs in O(n) time where n is the length of the strings, with O(1) space for the prefix sum arrays (constant size of 53).

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Given:

  • s = "abc"
  • t = "bcd"
  • nextCost = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
  • previousCost = [27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2]

Step 1: Build Prefix Sum Arrays

We create s1 for forward costs and s2 for backward costs, each of size 53.

For s1 (forward), we accumulate nextCost values twice:

  • s1[0] = 0
  • s1[1] = 0 + nextCost[0] = 0 + 1 = 1
  • s1[2] = 1 + nextCost[1] = 1 + 2 = 3
  • s1[3] = 3 + nextCost[2] = 3 + 3 = 6
  • s1[4] = 6 + nextCost[3] = 6 + 4 = 10
  • ... (continuing through all 52 positions)

For s2 (backward), we accumulate previousCost values:

  • s2[0] = 0
  • s2[1] = 0 + previousCost[1] = 0 + 26 = 26
  • s2[2] = 26 + previousCost[2] = 26 + 25 = 51
  • s2[3] = 51 + previousCost[3] = 51 + 24 = 75
  • s2[4] = 75 + previousCost[4] = 75 + 23 = 98
  • ... (continuing through all 52 positions)

Step 2: Transform Each Character

Character 1: 'a' → 'b'

  • Source position x = 0 ('a')
  • Target position y = 1 ('b')
  • Forward cost: Since y >= x, we use c1 = s1[1] - s1[0] = 1 - 0 = 1
  • Backward cost: Since x < y, we wrap around, so c2 = s2[0 + 26] - s2[1] = s2[26] - s2[1]
    • s2[26] equals the sum of all 26 previousCost values = 351
    • c2 = 351 - 26 = 325
  • Minimum cost: min(1, 325) = 1

Character 2: 'b' → 'c'

  • Source position x = 1 ('b')
  • Target position y = 2 ('c')
  • Forward cost: Since y >= x, we use c1 = s1[2] - s1[1] = 3 - 1 = 2
  • Backward cost: Since x < y, we wrap around, so c2 = s2[1 + 26] - s2[2] = s2[27] - s2[2]
    • s2[27] = s2[26] + previousCost[0] = 351 + 27 = 378
    • c2 = 378 - 51 = 327
  • Minimum cost: min(2, 327) = 2

Character 3: 'c' → 'd'

  • Source position x = 2 ('c')
  • Target position y = 3 ('d')
  • Forward cost: Since y >= x, we use c1 = s1[3] - s1[2] = 6 - 3 = 3
  • Backward cost: Since x < y, we wrap around, so c2 = s2[2 + 26] - s2[3] = s2[28] - s2[3]
    • s2[28] = 378 + previousCost[1] = 378 + 26 = 404
    • c2 = 404 - 75 = 329
  • Minimum cost: min(3, 329) = 3

Step 3: Calculate Total Cost Total minimum cost = 1 + 2 + 3 = 6

The algorithm efficiently finds that shifting each character forward by one position is the optimal strategy, with a total cost of 6.

Solution Implementation

1class Solution:
2    def shiftDistance(
3        self, s: str, t: str, nextCost: List[int], previousCost: List[int]
4    ) -> int:
5        # Number of letters in the alphabet
6        alphabet_size = 26
7      
8        # Create prefix sum arrays for forward and backward shifts
9        # We use double the alphabet size to handle wraparound cases
10        forward_prefix_sum = [0] * (alphabet_size * 2 + 1)
11        backward_prefix_sum = [0] * (alphabet_size * 2 + 1)
12      
13        # Build prefix sum arrays
14        # forward_prefix_sum[i] = total cost to shift from position 0 to position i (forward)
15        # backward_prefix_sum[i] = total cost to shift from position 0 to position i (backward)
16        for i in range(alphabet_size * 2):
17            # For forward shifts, use nextCost cyclically
18            forward_prefix_sum[i + 1] = forward_prefix_sum[i] + nextCost[i % alphabet_size]
19            # For backward shifts, use previousCost cyclically
20            # (i + 1) % alphabet_size handles the wraparound for backward movement
21            backward_prefix_sum[i + 1] = backward_prefix_sum[i] + previousCost[(i + 1) % alphabet_size]
22      
23        total_cost = 0
24      
25        # Process each character pair from source and target strings
26        for source_char, target_char in zip(s, t):
27            # Convert characters to indices (0-25)
28            source_index = ord(source_char) - ord('a')
29            target_index = ord(target_char) - ord('a')
30          
31            # Calculate forward shift cost
32            # If target is before source in alphabet, we need to wrap around
33            if target_index < source_index:
34                # Add alphabet_size to target_index to handle wraparound
35                forward_cost = forward_prefix_sum[target_index + alphabet_size] - forward_prefix_sum[source_index]
36            else:
37                # Direct forward shift without wraparound
38                forward_cost = forward_prefix_sum[target_index] - forward_prefix_sum[source_index]
39          
40            # Calculate backward shift cost
41            # If source is before target in alphabet, we need to wrap around
42            if source_index < target_index:
43                # Add alphabet_size to source_index to handle wraparound
44                backward_cost = backward_prefix_sum[source_index + alphabet_size] - backward_prefix_sum[target_index]
45            else:
46                # Direct backward shift without wraparound
47                backward_cost = backward_prefix_sum[source_index] - backward_prefix_sum[target_index]
48          
49            # Choose the minimum cost between forward and backward shifts
50            total_cost += min(forward_cost, backward_cost)
51      
52        return total_cost
53
1class Solution {
2    public long shiftDistance(String s, String t, int[] nextCost, int[] previousCost) {
3        int alphabetSize = 26;
4      
5        // Create prefix sum arrays for forward and backward shifts
6        // Arrays are doubled in size to handle circular wrapping
7        long[] forwardPrefixSum = new long[(alphabetSize << 1) + 1];
8        long[] backwardPrefixSum = new long[(alphabetSize << 1) + 1];
9      
10        // Build prefix sums for both forward and backward shifts
11        // Forward: cost of shifting from character i to i+1
12        // Backward: cost of shifting from character i to i-1
13        for (int i = 0; i < (alphabetSize << 1); i++) {
14            forwardPrefixSum[i + 1] = forwardPrefixSum[i] + nextCost[i % alphabetSize];
15            backwardPrefixSum[i + 1] = backwardPrefixSum[i] + previousCost[(i + 1) % alphabetSize];
16        }
17      
18        long totalCost = 0;
19      
20        // Process each character position in the strings
21        for (int i = 0; i < s.length(); i++) {
22            // Get character indices (0-25 for 'a'-'z')
23            int sourceChar = s.charAt(i) - 'a';
24            int targetChar = t.charAt(i) - 'a';
25          
26            // Calculate forward shift cost from sourceChar to targetChar
27            // If targetChar < sourceChar, we need to wrap around (add alphabetSize)
28            long forwardCost = forwardPrefixSum[targetChar + (targetChar < sourceChar ? alphabetSize : 0)] 
29                              - forwardPrefixSum[sourceChar];
30          
31            // Calculate backward shift cost from sourceChar to targetChar
32            // If sourceChar < targetChar, we need to wrap around (add alphabetSize)
33            long backwardCost = backwardPrefixSum[sourceChar + (sourceChar < targetChar ? alphabetSize : 0)] 
34                               - backwardPrefixSum[targetChar];
35          
36            // Choose the minimum cost between forward and backward shifts
37            totalCost += Math.min(forwardCost, backwardCost);
38        }
39      
40        return totalCost;
41    }
42}
43
1class Solution {
2public:
3    long long shiftDistance(string s, string t, vector<int>& nextCost, vector<int>& previousCost) {
4        const int ALPHABET_SIZE = 26;
5      
6        // Create prefix sum arrays for forward and backward shift costs
7        // Size is (2 * ALPHABET_SIZE + 1) to handle circular wrapping
8        vector<long long> forwardPrefixSum((ALPHABET_SIZE << 1) + 1);
9        vector<long long> backwardPrefixSum((ALPHABET_SIZE << 1) + 1);
10      
11        // Build prefix sums for both forward and backward shifts
12        // The arrays are doubled to handle circular nature of alphabet
13        for (int i = 0; i < (ALPHABET_SIZE << 1); ++i) {
14            // Forward shift: accumulate nextCost values
15            forwardPrefixSum[i + 1] = forwardPrefixSum[i] + nextCost[i % ALPHABET_SIZE];
16          
17            // Backward shift: accumulate previousCost values
18            // (i + 1) % ALPHABET_SIZE handles the circular indexing
19            backwardPrefixSum[i + 1] = backwardPrefixSum[i] + previousCost[(i + 1) % ALPHABET_SIZE];
20        }
21      
22        long long totalCost = 0;
23      
24        // Process each character position
25        for (int i = 0; i < s.size(); ++i) {
26            // Convert characters to indices (0-25)
27            int sourceIndex = s[i] - 'a';
28            int targetIndex = t[i] - 'a';
29          
30            // Calculate forward shift cost
31            // If target < source, we need to wrap around (add ALPHABET_SIZE)
32            long long forwardCost = forwardPrefixSum[targetIndex + (targetIndex < sourceIndex ? ALPHABET_SIZE : 0)] 
33                                   - forwardPrefixSum[sourceIndex];
34          
35            // Calculate backward shift cost
36            // If source < target, we need to wrap around (add ALPHABET_SIZE)
37            long long backwardCost = backwardPrefixSum[sourceIndex + (sourceIndex < targetIndex ? ALPHABET_SIZE : 0)] 
38                                    - backwardPrefixSum[targetIndex];
39          
40            // Choose the minimum cost between forward and backward shift
41            totalCost += min(forwardCost, backwardCost);
42        }
43      
44        return totalCost;
45    }
46};
47
1/**
2 * Calculates the minimum cost to transform string s into string t
3 * by shifting characters forward or backward in the alphabet (circular).
4 * 
5 * @param s - Source string to transform
6 * @param t - Target string to transform into
7 * @param nextCost - Array of costs for shifting each letter forward (a->b, b->c, ..., z->a)
8 * @param previousCost - Array of costs for shifting each letter backward (b->a, c->b, ..., a->z)
9 * @returns The minimum total cost to transform s into t
10 */
11function shiftDistance(s: string, t: string, nextCost: number[], previousCost: number[]): number {
12    const ALPHABET_SIZE = 26;
13  
14    // Create prefix sum arrays for forward and backward costs
15    // Double the size to handle circular wrapping (z -> a, etc.)
16    const forwardPrefixSum: number[] = Array((ALPHABET_SIZE << 1) + 1).fill(0);
17    const backwardPrefixSum: number[] = Array((ALPHABET_SIZE << 1) + 1).fill(0);
18  
19    // Build prefix sums for forward shifts (using nextCost)
20    // and backward shifts (using previousCost)
21    for (let i = 0; i < ALPHABET_SIZE << 1; i++) {
22        // Forward shift costs: accumulate nextCost values (circular)
23        forwardPrefixSum[i + 1] = forwardPrefixSum[i] + nextCost[i % ALPHABET_SIZE];
24      
25        // Backward shift costs: accumulate previousCost values (circular)
26        // Note: previousCost[(i + 1) % m] because previousCost[0] is cost from 'b' to 'a'
27        backwardPrefixSum[i + 1] = backwardPrefixSum[i] + previousCost[(i + 1) % ALPHABET_SIZE];
28    }
29  
30    let totalMinCost = 0;
31    const ASCII_OFFSET_A = 'a'.charCodeAt(0);
32  
33    // Process each character pair from s and t
34    for (let i = 0; i < s.length; i++) {
35        // Convert characters to indices (0-25)
36        const sourceIndex = s.charCodeAt(i) - ASCII_OFFSET_A;
37        const targetIndex = t.charCodeAt(i) - ASCII_OFFSET_A;
38      
39        // Calculate forward shift cost from sourceIndex to targetIndex
40        // If target < source, we need to wrap around (add ALPHABET_SIZE to target)
41        const forwardCost = forwardPrefixSum[targetIndex + (targetIndex < sourceIndex ? ALPHABET_SIZE : 0)] 
42                           - forwardPrefixSum[sourceIndex];
43      
44        // Calculate backward shift cost from sourceIndex to targetIndex
45        // If source < target, we need to wrap around (add ALPHABET_SIZE to source)
46        const backwardCost = backwardPrefixSum[sourceIndex + (sourceIndex < targetIndex ? ALPHABET_SIZE : 0)] 
47                            - backwardPrefixSum[targetIndex];
48      
49        // Choose the minimum cost between forward and backward shifts
50        totalMinCost += Math.min(forwardCost, backwardCost);
51    }
52  
53    return totalMinCost;
54}
55

Time and Space Complexity

Time Complexity: O(m + n) where m = 26 (the size of the alphabet) and n is the length of strings s and t.

  • The first loop runs 2m iterations to build the prefix sum arrays s1 and s2, which takes O(m) time.
  • The second loop iterates through each character pair in strings s and t, which takes O(n) time.
  • Inside the second loop, all operations (character conversion, array lookups, and arithmetic) are O(1).
  • Since m is a constant (26), the overall time complexity simplifies to O(n).

Space Complexity: O(m) where m = 26.

  • Two arrays s1 and s2 are created, each of size 2m + 1 = 53, which requires O(m) space.
  • All other variables (ans, x, y, c1, c2, loop counters) use constant space O(1).
  • Since m is a constant (26), the space complexity is effectively O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Index Mapping for Backward Shifts

The Pitfall: When building the backward prefix sum array, a common mistake is using previousCost[i % m] instead of previousCost[(i + 1) % m]. This error occurs because developers often assume the indexing works the same way as forward shifts.

Why It's Wrong: When moving backward from position i, you're actually using the previousCost of the character you're moving FROM, not TO. For example:

  • Moving backward from 'b' (index 1) to 'a' (index 0) uses previousCost[1] (the cost of shifting 'b' backward)
  • Moving backward from 'a' (index 0) to 'z' (index 25) uses previousCost[0] (the cost of shifting 'a' backward)

Incorrect Code:

# WRONG - This uses the wrong index for backward costs
for i in range(m << 1):
    backward_prefix_sum[i + 1] = backward_prefix_sum[i] + previousCost[i % m]  # Bug!

Correct Code:

# CORRECT - Uses (i + 1) % m to get the proper cost index
for i in range(m << 1):
    backward_prefix_sum[i + 1] = backward_prefix_sum[i] + previousCost[(i + 1) % m]

2. Misunderstanding Wraparound Logic

The Pitfall: Developers might incorrectly calculate the wraparound costs by not properly adjusting the indices when the transformation requires going "around" the alphabet.

Why It's Wrong: When transforming 'z' to 'b' forward, you need to go z→a→b, which wraps around. Without adding the alphabet size to handle this wraparound in the prefix sum calculation, you'd get negative or incorrect costs.

Incorrect Approach:

# WRONG - Doesn't handle wraparound properly
forward_cost = forward_prefix_sum[target_index] - forward_prefix_sum[source_index]
backward_cost = backward_prefix_sum[source_index] - backward_prefix_sum[target_index]

Correct Approach:

# CORRECT - Adds alphabet_size when wraparound is needed
if target_index < source_index:
    forward_cost = forward_prefix_sum[target_index + alphabet_size] - forward_prefix_sum[source_index]
else:
    forward_cost = forward_prefix_sum[target_index] - forward_prefix_sum[source_index]

if source_index < target_index:
    backward_cost = backward_prefix_sum[source_index + alphabet_size] - backward_prefix_sum[target_index]
else:
    backward_cost = backward_prefix_sum[source_index] - backward_prefix_sum[target_index]

3. Off-by-One Errors in Prefix Sum Array Size

The Pitfall: Using size 26 * 2 instead of 26 * 2 + 1 for the prefix sum arrays, or using incorrect bounds in the loop.

Why It's Wrong: Prefix sum arrays need an extra element at the beginning (index 0) to represent the sum of zero elements. Without this, array access will be out of bounds or calculations will be off.

Solution: Always allocate (alphabet_size * 2 + 1) elements for the prefix sum arrays to accommodate:

  • Index 0: sum of zero elements
  • Indices 1-52: cumulative sums for double alphabet cycle
Discover Your Strengths and Weaknesses: Take Our 5-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