Facebook Pixel

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 positions
  • shifts[1] = 5 means shift the first 2 letters (s[0] and s[1]) by 5 positions
  • shifts[2] = 9 means shift the first 3 letters (s[0], s[1], and s[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:

  1. Adds shifts[i] to the running total t
  2. Calculates the new character position by taking (original_position + t) % 26
  3. Converts back to the corresponding letter

The function returns the final string after all shifts have been applied.

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

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 by shifts[0] + shifts[1] + shifts[2] + ... + shifts[n-1]
  • Character at index 1 gets shifted by shifts[1] + shifts[2] + ... + shifts[n-1]
  • Character at index 2 gets shifted by shifts[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:

  1. Initialize variables: We start with n as the length of the string, t as our running total (initialized to 0), and convert the string s to a list for in-place modification.

  2. Traverse backwards: We iterate through indices from n-1 down to 0 using range(n - 1, -1, -1). This backwards traversal is crucial for building the suffix sum efficiently.

  3. Accumulate shifts: For each position i:

    • Add shifts[i] to the running total t. This t now represents the total number of shifts needed for the character at position i.
    • 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]
  4. 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]
  5. 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 Evaluator

Example 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 positions
  • shifts[1] = 20 means shift the first 2 letters (s[0]='b' and s[1]='a') by 20 positions
  • shifts[2] = 30 means shift the first 3 letters (s[0]='b', s[1]='a', and s[2]='d') by 30 positions

So the total shifts for each character are:

  • s[0]='b' gets shifted by 10 + 20 + 30 = 60 positions
  • s[1]='a' gets shifted by 20 + 30 = 50 positions
  • s[2]='d' gets shifted by 30 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 to t: 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 to t: 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 to t: 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

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

Load More