2381. Shifting Letters II - Explanation
Description
You are given a lowercase string s and operations in shifts. Each operation [start, end, direction] changes every character in the inclusive range from start to end by one alphabet position.
A direction of 1 moves forward, with z wrapping to a. A direction of 0 moves backward, with a wrapping to z. Apply all operations and return the final string.
Example 1:
Input: s = "az"
shifts = [[0,1,1]]
Output: "ba"Explanation: Shift both characters forward once: a becomes b, and z wraps around to a. The result is "ba".
Example 2:
Input: s = "aaa"
shifts = [[0,2,0],[1,1,1]]
Output: "zaz"Explanation: The first operation shifts all three a characters backward, producing "zzz". The second shifts only the middle character forward, so the final string is "zaz".
Example 3:
Input: s = "xyz"
shifts = [[0,1,1],[1,2,0]]
Output: "yyy"Explanation: The first operation changes "xyz" to "yzz". The second moves the last two characters backward, resulting in "yyy". The shifts cancel at the overlapping middle position.
Constraints:
1 <= s.length <= 500001 <= shifts.length <= 50000scontains only lowercase English letters.- Each shift contains exactly three integers:
[start, end, direction]. 0 <= start <= end < s.lengthdirectionis either0or1.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Arrays and String Manipulation - Understanding how to iterate and modify character arrays using ASCII values and modular arithmetic
- Prefix Sum / Difference Arrays - The sweep line technique uses difference arrays to efficiently apply range updates, which is essential for the optimal solution
- Modular Arithmetic - Handling wraparound when shifting characters beyond 'z' or before 'a' requires understanding modulo operations with negative numbers
1. Brute Force
Intuition
The straightforward approach is to apply each shift operation directly. For each shift, we iterate through the specified range and increment or decrement each character. Since characters wrap around the alphabet, we use modulo 26 arithmetic.
This approach is simple but slow because we might repeatedly process the same characters across multiple overlapping shifts.
Algorithm
- Convert the string to an array of integers (0-25 representing 'a'-'z').
- For each shift
[l, r, d]:- Iterate through indices from
ltor. - Add
1if direction is forward, subtract1if backward. - Apply modulo
26to handle wraparound.
- Iterate through indices from
- Convert the integer array back to characters.
- Return the resulting string.
Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the size of the array .
2. Sweep Line Algorithm
Intuition
Instead of applying each shift individually, we can use a difference array technique. The idea is to mark where shifts begin and end, then compute the cumulative effect as we scan through the string.
For a shift affecting range [l, r], we add the shift value at index l and subtract it at index r + 1. When we compute the running sum (prefix sum) of this difference array, each position automatically accumulates the total shift from all overlapping operations.
Algorithm
- Create a difference array
prefix_diffof sizen + 1, initialized to zeros. - For each shift
[l, r, d]:- Add
+1or-1(based on direction) at indexl. - Subtract the same value at index
r + 1.
- Add
- Compute the running sum while traversing the string:
- Maintain a cumulative
diffvariable. - For each index, add the net shift to the character.
- Apply modulo
26arithmetic for wraparound.
- Maintain a cumulative
- Return the resulting string.
class Solution:
def shiftingLetters(self, s: str, shifts: List[List[int]]) -> str:
prefix_diff = [0] * (len(s) + 1)
for left, right, d in shifts:
val = 1 if d == 1 else -1
prefix_diff[left] += val
prefix_diff[right + 1] -= val
diff = 0
res = [ord(c) - ord("a") for c in s]
for i in range(len(s)):
diff += prefix_diff[i]
res[i] = (diff + res[i] + 26) % 26
s = [chr(ord("a") + n) for n in res]
return "".join(s)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the size of the array .
3. Binary Indexed Tree (Fenwick Tree)
Intuition
A Binary Indexed Tree (BIT) efficiently handles range updates and point queries. For this problem, we use the BIT to support range updates: when we need to add a value to all elements in range [l, r], we update at position l and cancel at position r + 1.
When querying, the prefix sum at any index gives us the total accumulated shift for that position. This is useful when shifts need to be applied dynamically or when we need to query intermediate results.
Algorithm
- Initialize a BIT of size
n + 2. - For each shift
[l, r, d]:- Perform a range update by calling
update(l, delta)andupdate(r + 1, -delta), wheredeltais+1or-1.
- Perform a range update by calling
- For each character in the string:
- Query the BIT to get the total shift at that position.
- Apply the shift with modulo
26arithmetic. - Build the result character.
- Return the resulting string.
class BIT:
def __init__(self, size):
self.n = size + 2
self.tree = [0] * self.n
def update(self, index, delta):
index += 1
while index < self.n:
self.tree[index] += delta
index += index & -index
def prefix_sum(self, index):
index += 1
total = 0
while index > 0:
total += self.tree[index]
index -= index & -index
return total
def range_update(self, left, right, delta):
self.update(left, delta)
self.update(right + 1, -delta)
class Solution:
def shiftingLetters(self, s: str, shifts: List[List[int]]) -> str:
n = len(s)
bit = BIT(n)
for left, right, d in shifts:
delta = 1 if d == 1 else -1
bit.range_update(left, right, delta)
res = []
for i in range(n):
shift = bit.prefix_sum(i) % 26
code = (ord(s[i]) - ord('a') + shift + 26) % 26
res.append(chr(ord('a') + code))
return ''.join(res)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the length of the string and is the size of the array .
Common Pitfalls
Forgetting to Handle Negative Modulo
When shifting characters backward, the intermediate result can become negative before applying modulo 26. In many programming languages, the modulo of a negative number returns a negative result (e.g., -1 % 26 = -1 in some languages). Always add 26 before taking the modulo to ensure the result is positive: ((shift + char) % 26 + 26) % 26.
Off-by-One Error in Difference Array
When using the sweep line or difference array technique, the cancellation must happen at index r + 1, not at r. Placing the cancellation at index r means the shift will not be applied to the last character in the range. Remember that prefix_diff[right + 1] -= val ensures the shift applies to all indices from left through right inclusive.
Overlooking Large Accumulated Shifts
With many overlapping shifts, the cumulative value can grow very large (positive or negative). Taking modulo only at the end of all accumulations is correct, but some implementations attempt to normalize after each shift, which can introduce subtle bugs. Ensure you handle the final modulo operation correctly after summing all prefix values.
Sign in to join the discussion