848. Shifting Letters
Problem Description
You have a string s
containing only lowercase English letters and an integer array shifts
with the same length as the string.
The problem introduces a shift operation where each letter moves to the next letter in the alphabet. When you reach 'z', it wraps around to 'a'. For example:
shift('a') = 'b'
shift('t') = 'u'
shift('z') = 'a'
The key rule is: for each shifts[i] = x
, you need to shift the first i + 1
letters of the string by x
positions.
Let's break this down with an example. If s = "abc"
and shifts = [3, 5, 9]
:
shifts[0] = 3
means shift the first 1 letter (s[0]
) by 3 positionsshifts[1] = 5
means shift the first 2 letters (s[0]
ands[1]
) by 5 positionsshifts[2] = 9
means shift the first 3 letters (s[0]
,s[1]
, ands[2]
) by 9 positions
Notice that s[0]
gets shifted by 3 + 5 + 9 = 17
total positions, s[1]
gets shifted by 5 + 9 = 14
positions, and s[2]
gets shifted by 9
positions.
The solution uses a suffix sum approach, traversing from right to left to accumulate the total shifts for each character. For each character at position i
, it:
- Adds
shifts[i]
to the running totalt
- Calculates the new character position by taking
(original_position + t) % 26
- Converts back to the corresponding letter
The function returns the final string after all shifts have been applied.
Intuition
The first observation is that each character gets shifted multiple times. If we look at how many times each character gets shifted:
- Character at index
0
gets shifted byshifts[0] + shifts[1] + shifts[2] + ... + shifts[n-1]
- Character at index
1
gets shifted byshifts[1] + shifts[2] + ... + shifts[n-1]
- Character at index
2
gets shifted byshifts[2] + ... + shifts[n-1]
- And so on...
We can see a pattern here - each character at position i
needs to be shifted by the sum of all values from shifts[i]
to the end of the array. This is essentially a suffix sum pattern.
Instead of calculating the sum for each character separately (which would involve redundant calculations), we can be clever about it. If we traverse the string from right to left:
- For the last character, we only need
shifts[n-1]
- For the second-to-last character, we need
shifts[n-2] + shifts[n-1]
- For the third-to-last character, we need
shifts[n-3] + shifts[n-2] + shifts[n-1]
By going backwards, we can maintain a running total t
that accumulates the shift amounts. At each position i
, we just add shifts[i]
to our running total, and that gives us the total shift amount for character i
.
The key insight is that working backwards allows us to build up these suffix sums efficiently in a single pass, avoiding the need to recalculate overlapping sums. Each character's final position can then be computed using modular arithmetic: (original_position + total_shifts) % 26
to handle the wraparound from 'z' to 'a'.
Learn more about Prefix Sum patterns.
Solution Approach
The solution implements the suffix sum approach by traversing the string from right to left. Here's how the algorithm works:
-
Initialize variables: We start with
n
as the length of the string,t
as our running total (initialized to 0), and convert the strings
to a list for in-place modification. -
Traverse backwards: We iterate through indices from
n-1
down to0
usingrange(n - 1, -1, -1)
. This backwards traversal is crucial for building the suffix sum efficiently. -
Accumulate shifts: For each position
i
:- Add
shifts[i]
to the running totalt
. Thist
now represents the total number of shifts needed for the character at positioni
. - The running total works because:
- At position
n-1
:t = shifts[n-1]
- At position
n-2
:t = shifts[n-2] + shifts[n-1]
- At position
i
:t = shifts[i] + shifts[i+1] + ... + shifts[n-1]
- At position
- Add
-
Calculate new character:
- First, find the current character's position in the alphabet:
ord(s[i]) - ord("a")
gives us a value from 0 to 25 - Add the total shift amount
t
to this position - Apply modulo 26 to handle wraparound:
j = (ord(s[i]) - ord("a") + t) % 26
- Convert back to a character using
ascii_lowercase[j]
- First, find the current character's position in the alphabet:
-
Return result: After processing all characters, join the list back into a string and return it.
The time complexity is O(n)
where n
is the length of the string, as we make a single pass through the array. The space complexity is O(n)
for converting the string to a list (strings are immutable in Python, so we need a mutable structure for in-place updates).
The elegance of this solution lies in recognizing that we can compute all suffix sums in a single backward pass, avoiding redundant calculations that would occur if we computed each character's total shift independently.
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 walk through a small example with s = "bad"
and shifts = [10, 20, 30]
.
Step 1: Understanding what shifts mean
shifts[0] = 10
means shift the first 1 letter (s[0]='b'
) by 10 positionsshifts[1] = 20
means shift the first 2 letters (s[0]='b'
ands[1]='a'
) by 20 positionsshifts[2] = 30
means shift the first 3 letters (s[0]='b'
,s[1]='a'
, ands[2]='d'
) by 30 positions
So the total shifts for each character are:
s[0]='b'
gets shifted by10 + 20 + 30 = 60
positionss[1]='a'
gets shifted by20 + 30 = 50
positionss[2]='d'
gets shifted by30
positions
Step 2: Apply the suffix sum approach (traverse right to left)
Convert s = "bad"
to a list: ['b', 'a', 'd']
Initialize t = 0
(running total)
Iteration 1 (i = 2, character = 'd'):
- Add
shifts[2] = 30
tot
:t = 0 + 30 = 30
- Current character 'd' has position 3 in alphabet (0-indexed)
- New position:
(3 + 30) % 26 = 33 % 26 = 7
- Character at position 7 is 'h'
- Update:
s[2] = 'h'
- Current state:
['b', 'a', 'h']
Iteration 2 (i = 1, character = 'a'):
- Add
shifts[1] = 20
tot
:t = 30 + 20 = 50
- Current character 'a' has position 0 in alphabet
- New position:
(0 + 50) % 26 = 50 % 26 = 24
- Character at position 24 is 'y'
- Update:
s[1] = 'y'
- Current state:
['b', 'y', 'h']
Iteration 3 (i = 0, character = 'b'):
- Add
shifts[0] = 10
tot
:t = 50 + 10 = 60
- Current character 'b' has position 1 in alphabet
- New position:
(1 + 60) % 26 = 61 % 26 = 9
- Character at position 9 is 'j'
- Update:
s[0] = 'j'
- Current state:
['j', 'y', 'h']
Step 3: Return the result
Join the list back to string: "jyh"
The final answer is "jyh"
.
Solution Implementation
1from typing import List
2from string import ascii_lowercase
3
4class Solution:
5 def shiftingLetters(self, s: str, shifts: List[int]) -> str:
6 # Get the length of the string
7 length = len(s)
8
9 # Convert string to list for in-place modification
10 char_list = list(s)
11
12 # Initialize cumulative shift counter
13 cumulative_shift = 0
14
15 # Process characters from right to left
16 for index in range(length - 1, -1, -1):
17 # Add current shift value to cumulative sum
18 cumulative_shift += shifts[index]
19
20 # Calculate the new character position after shifting
21 # Convert character to 0-based index (a=0, b=1, ..., z=25)
22 current_char_index = ord(char_list[index]) - ord('a')
23
24 # Apply the cumulative shift and wrap around using modulo 26
25 new_char_index = (current_char_index + cumulative_shift) % 26
26
27 # Replace the character with the shifted one
28 char_list[index] = ascii_lowercase[new_char_index]
29
30 # Convert the list back to string and return
31 return ''.join(char_list)
32
1class Solution {
2 public String shiftingLetters(String s, int[] shifts) {
3 // Convert string to character array for easier manipulation
4 char[] characters = s.toCharArray();
5 int length = characters.length;
6
7 // Track cumulative shift amount
8 long cumulativeShift = 0;
9
10 // Process characters from right to left
11 // Each character gets shifted by the sum of all shifts from its position to the end
12 for (int i = length - 1; i >= 0; i--) {
13 // Add current position's shift to cumulative total
14 cumulativeShift += shifts[i];
15
16 // Calculate new character position after shifting
17 // Convert character to 0-25 range, add shift, then modulo 26 to wrap around
18 int shiftedPosition = (int) ((characters[i] - 'a' + cumulativeShift) % 26);
19
20 // Convert back to character and update array
21 characters[i] = (char) ('a' + shiftedPosition);
22 }
23
24 // Convert character array back to string and return
25 return String.valueOf(characters);
26 }
27}
28
1class Solution {
2public:
3 string shiftingLetters(string s, vector<int>& shifts) {
4 // Total cumulative shift amount
5 long long cumulativeShift = 0;
6 int stringLength = s.size();
7
8 // Process from right to left to accumulate shifts
9 // Each character gets shifted by the sum of all shifts from its position to the end
10 for (int i = stringLength - 1; i >= 0; --i) {
11 // Add current position's shift to cumulative total
12 cumulativeShift += shifts[i];
13
14 // Calculate the new character position after shifting
15 // Convert character to 0-based index, add shift, then modulo 26 to wrap around
16 int newCharIndex = (s[i] - 'a' + cumulativeShift) % 26;
17
18 // Update the character at current position
19 s[i] = 'a' + newCharIndex;
20 }
21
22 return s;
23 }
24};
25
1function shiftingLetters(s: string, shifts: number[]): string {
2 // Total cumulative shift amount
3 let cumulativeShift: number = 0;
4 const stringLength: number = s.length;
5
6 // Convert string to array for easier manipulation since strings are immutable in TypeScript
7 const charArray: string[] = s.split('');
8
9 // Process from right to left to accumulate shifts
10 // Each character gets shifted by the sum of all shifts from its position to the end
11 for (let i = stringLength - 1; i >= 0; i--) {
12 // Add current position's shift to cumulative total
13 cumulativeShift += shifts[i];
14
15 // Calculate the new character position after shifting
16 // Convert character to 0-based index (a=0, b=1, etc.), add shift, then modulo 26 to wrap around
17 const currentCharCode: number = charArray[i].charCodeAt(0) - 'a'.charCodeAt(0);
18 const newCharIndex: number = (currentCharCode + cumulativeShift) % 26;
19
20 // Update the character at current position by converting back to character
21 charArray[i] = String.fromCharCode('a'.charCodeAt(0) + newCharIndex);
22 }
23
24 // Join the array back into a string and return
25 return charArray.join('');
26}
27
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the string s
. The algorithm iterates through the string once from right to left, performing constant-time operations for each character (addition, modulo operation, character replacement).
Space Complexity: O(n)
for the total space used, or O(1)
if we don't count the space needed for the output. The algorithm converts the string to a list which takes O(n)
space. However, if we consider only the auxiliary space beyond what's needed to store the answer (following the reference's convention of "ignoring the space consumption of the answer"), the space complexity is O(1)
as we only use a constant amount of extra variables (n
, t
, i
, j
).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Shift Values
The Pitfall: When accumulating shift values, the cumulative sum can become extremely large. In languages with fixed integer sizes, this could cause overflow. Even in Python (which handles arbitrary precision integers), very large numbers can impact performance and may cause issues when computing modulo operations.
Consider this example:
shifts = [10^9, 10^9, 10^9, ...]
with many elements- The cumulative sum quickly grows to astronomical values
The Solution: Apply modulo 26 to the cumulative shift at each step, not just when calculating the new character position. Since we only care about the effective shift (shifts modulo 26), we can keep the cumulative sum bounded:
def shiftingLetters(self, s: str, shifts: List[int]) -> str:
length = len(s)
char_list = list(s)
cumulative_shift = 0
for index in range(length - 1, -1, -1):
# Apply modulo to keep cumulative_shift bounded
cumulative_shift = (cumulative_shift + shifts[index]) % 26
current_char_index = ord(char_list[index]) - ord('a')
new_char_index = (current_char_index + cumulative_shift) % 26
char_list[index] = ascii_lowercase[new_char_index]
return ''.join(char_list)
2. Direction Confusion - Forward vs Backward Iteration
The Pitfall: Attempting to build the suffix sum while iterating forward (from index 0 to n-1) leads to incorrect results. This happens because:
- At position 0, you'd need to know the sum of all shifts from 0 to n-1
- At position 1, you'd need the sum from 1 to n-1
- This requires either pre-computing all suffix sums or making multiple passes
The Solution: Always iterate backward when building suffix sums. The backward iteration naturally accumulates the correct total shifts for each position in a single pass.
3. Off-by-One Errors with Character Arithmetic
The Pitfall: Incorrectly handling the character-to-number conversion can lead to wrong results:
# Wrong: forgetting to subtract ord('a')
new_char_index = (ord(char_list[index]) + cumulative_shift) % 26
# This treats 'a' as 97 instead of 0, giving incorrect modulo results
The Solution: Always normalize characters to 0-based indices before applying shifts:
current_char_index = ord(char_list[index]) - ord('a') # 'a' -> 0, 'b' -> 1, etc.
new_char_index = (current_char_index + cumulative_shift) % 26
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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!