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]
, wherej
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]
, wherej
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
.
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:
- Moving forward (clockwise) through the alphabet
- 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:
- Calculate the forward path cost using the forward prefix sum array
- Calculate the backward path cost using the backward prefix sum array
- 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 accumulatenextCost[i % 26]
to handle forward movement costs. The modulo operation ensures we cycle through the 26 costs twice. - For
s2
: We accumulatepreviousCost[(i + 1) % 26]
for backward movement. The(i + 1) % m
indexing accounts for the fact that moving backward from positioni
uses the cost at positioni
.
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
): Ify < x
(target comes before source in alphabet), we need to wrap around, so we usey + 26
as the endpoint. Otherwise, we usey
directly. The cost iss1[endpoint] - s1[x]
. -
Backward cost (
c2
): Ifx < y
(source comes before target), we need to wrap around backward, so we usex + 26
as the starting point. The cost iss2[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 EvaluatorExample 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 usec1 = s1[1] - s1[0] = 1 - 0 = 1
- Backward cost: Since
x < y
, we wrap around, soc2 = s2[0 + 26] - s2[1] = s2[26] - s2[1]
s2[26]
equals the sum of all 26previousCost
values = 351c2 = 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 usec1 = s1[2] - s1[1] = 3 - 1 = 2
- Backward cost: Since
x < y
, we wrap around, soc2 = 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 usec1 = s1[3] - s1[2] = 6 - 3 = 3
- Backward cost: Since
x < y
, we wrap around, soc2 = 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 arrayss1
ands2
, which takesO(m)
time. - The second loop iterates through each character pair in strings
s
andt
, which takesO(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 toO(n)
.
Space Complexity: O(m)
where m = 26
.
- Two arrays
s1
ands2
are created, each of size2m + 1 = 53
, which requiresO(m)
space. - All other variables (
ans
,x
,y
,c1
,c2
, loop counters) use constant spaceO(1)
. - Since
m
is a constant (26), the space complexity is effectivelyO(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
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!