1625. Lexicographically Smallest String After Applying Operations
Problem Description
You have a string s
containing only digits (0-9) with even length, and two integers a
and b
. You can perform two types of operations on this string any number of times in any order:
Operation 1 - Add to odd indices: Add the value a
to all digits at odd positions (using 0-based indexing). If a digit exceeds 9 after addition, it wraps around cyclically back to 0-9 range. For instance, if you have s = "3456"
and a = 5
, the digits at odd indices (positions 1 and 3) are '4' and '6'. After adding 5, they become '9' and '1' (since 6+5=11, which becomes 1), resulting in "3951"
.
Operation 2 - Rotate right: Shift the string to the right by b
positions. The characters that move past the end wrap around to the beginning. For example, with s = "3456"
and b = 1
, rotating right by 1 position moves the last character '6' to the front, giving "6345"
.
Your goal is to find the lexicographically smallest string possible after applying these operations any number of times.
A string is lexicographically smaller than another if, at the first position where they differ, it has a character that comes earlier in alphabetical/numerical order. For example, "0158"
is smaller than "0190"
because at position 2 (0-indexed), '5' comes before '9'.
The solution uses BFS to explore all possible strings that can be generated through these operations. It maintains a visited set to avoid processing the same string multiple times and keeps track of the minimum string encountered during the exploration.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem can be modeled as a graph where each string state is a node, and edges represent transformations (either adding
a
to odd indices or rotating byb
positions).
Is it a tree?
- No: The graph is not a tree because multiple paths can lead to the same string state (cycles exist), and there's no hierarchical parent-child relationship.
Is the problem related to directed acyclic graphs?
- No: The graph contains cycles since we can apply operations that might eventually return to a previous state.
Is the problem related to shortest paths?
- No: We're not looking for the shortest path between states. Instead, we need to explore all reachable states to find the lexicographically smallest string.
Does the problem involve connectivity?
- No: We're not concerned with whether nodes are connected or finding connected components.
Does the problem have small constraints?
- Yes: The string length is limited (even length string of digits), and the state space is bounded by the possible unique strings we can generate through transformations.
DFS/backtracking?
- Yes: We need to explore all possible string states reachable from the initial string through the two operations, making DFS an appropriate choice.
Conclusion: The flowchart suggests using DFS to explore all reachable string states. However, the provided solution actually uses BFS (with a queue) instead of DFS. Both approaches work for this problem since we need to visit all reachable states regardless of the order. The BFS approach systematically explores states level by level, while keeping track of visited states to avoid infinite loops, and maintains the minimum string encountered during the exploration.
Intuition
The key insight is recognizing that this problem is about exploring a state space where each state is a string, and we can transition between states using two operations. Since we want the lexicographically smallest string among all possible strings we can create, we need to explore all reachable states.
Think of it like this: starting from our initial string, we can create new strings by either adding a
to odd-indexed digits or rotating the string. Each new string we create can then be transformed again using these same operations. This creates a graph of string states connected by transformation operations.
Why can't we use a greedy approach? Because the operations interact in complex ways. Adding to odd indices might not immediately give us a smaller string, but after a rotation, those modified digits might move to positions that make the overall string smaller. Similarly, a rotation alone might not help, but combined with additions, it could lead to the optimal result.
Since the string has finite length and each digit can only be 0-9, the total number of unique strings we can generate is bounded. This means we can afford to explore all possibilities. The challenge is avoiding infinite loops - we might apply operations that bring us back to a string we've already seen.
This naturally leads to a graph traversal approach where:
- Each unique string is a node
- We keep track of visited strings to avoid cycles
- We explore all reachable strings and keep track of the minimum
The solution uses BFS with a queue, but DFS would work equally well. The choice of BFS here ensures we explore states level by level, but since we need to visit all reachable states anyway to find the minimum, the traversal order doesn't affect correctness. The visited set is crucial to prevent infinite exploration of the same states.
Learn more about Depth-First Search and Breadth-First Search patterns.
Solution Approach
The solution implements a BFS traversal to explore all possible string transformations:
1. Initialize the BFS Structure:
- Create a queue
q
initialized with the original strings
- Create a visited set
vis
to track strings we've already processed - Initialize
ans
with the original string as our current minimum
2. BFS Exploration: While the queue is not empty:
- Dequeue a string
s
from the front - Update
ans
if the current string is lexicographically smaller:if ans > s: ans = s
3. Generate Transformations: For each dequeued string, create two new strings:
Operation 1 - Add to odd indices (t1
):
t1 = ''.join([str((int(c) + a) % 10) if i & 1 else c for i, c in enumerate(s)])
- Iterate through each character with its index
- Check if index is odd using bitwise AND:
i & 1
(returns 1 for odd, 0 for even) - If odd: add
a
to the digit and use modulo 10 to handle wraparound - If even: keep the digit unchanged
- Join all characters back into a string
Operation 2 - Rotate right (t2
):
t2 = s[-b:] + s[:-b]
- Take the last
b
characters:s[-b:]
- Concatenate with all characters except the last
b
:s[:-b]
- This effectively rotates the string right by
b
positions
4. Process New States:
For each generated string t
in (t1, t2)
:
- Check if we've seen this string before:
if t not in vis
- If it's new:
- Add to visited set:
vis.add(t)
- Add to queue for further exploration:
q.append(t)
- Add to visited set:
5. Return the Result:
After exploring all reachable states, return ans
which contains the lexicographically smallest string found.
The algorithm guarantees finding the optimal solution because:
- It explores all possible string states reachable through any combination of operations
- The visited set ensures we don't process the same state twice, preventing infinite loops
- By comparing every encountered string with our current minimum, we're guaranteed to find the lexicographically smallest one
The time complexity depends on the number of unique states, which is bounded by the possible distinct strings we can generate from the operations.
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 with s = "2050"
, a = 3
, and b = 2
.
Initial Setup:
- Queue:
["2050"]
- Visited:
{"2050"}
- Current minimum:
"2050"
Iteration 1 - Process "2050":
- Dequeue
"2050"
- Compare with minimum:
"2050"
(no change) - Generate transformations:
- Operation 1 (add 3 to odd indices):
- Index 0: '2' (even) β keep '2'
- Index 1: '0' (odd) β (0+3)%10 = '3'
- Index 2: '5' (even) β keep '5'
- Index 3: '0' (odd) β (0+3)%10 = '3'
- Result:
t1 = "2353"
- Operation 2 (rotate right by 2):
- Last 2 chars: "50"
- First 2 chars: "20"
- Result:
t2 = "5020"
- Operation 1 (add 3 to odd indices):
- Check if new:
"2353"
not visited β add to queue and visited set"5020"
not visited β add to queue and visited set
- Queue:
["2353", "5020"]
Iteration 2 - Process "2353":
- Dequeue
"2353"
- Compare with minimum:
"2050"
<"2353"
(no change) - Generate transformations:
- Operation 1:
"2656"
(add 3 to positions 1,3: 3β6, 3β6) - Operation 2:
"5323"
(rotate right by 2)
- Operation 1:
- Add unvisited strings to queue
Iteration 3 - Process "5020":
- Dequeue
"5020"
- Compare with minimum:
"2050"
<"5020"
(no change) - Generate transformations:
- Operation 1:
"5323"
(add 3 to positions 1,3: 0β3, 0β3) - Operation 2:
"2050"
(rotate right by 2)
- Operation 1:
"5323"
already visited,"2050"
already visited β skip both
Continue processing...
Eventually, we'll encounter "0225"
:
- This can be reached by:
"2050"
β"5020"
β"5323"
β"2353"
β"5323"
β"2353"
β"5323"
β"5326"
β"2653"
β"2956"
β"5629"
β"2956"
β"5629"
β"2956"
β"5629"
β"2956"
β"5629"
β"2952"
β"5229"
β"2952"
β"5225"
β"2552"
β"5225"
β"2552"
β"2855"
β"5528"
β"2855"
β"5528"
β"2855"
β"5528"
β"5225"
β"2552"
β"5225"
β"0225"
- Compare with minimum:
"0225"
<"2050"
β update minimum to"0225"
The process continues until all reachable states are explored. The final answer is "0225"
, which is the lexicographically smallest string achievable through any combination of the two operations.
Solution Implementation
1from collections import deque
2from typing import Set
3
4class Solution:
5 def findLexSmallestString(self, s: str, a: int, b: int) -> str:
6 # Initialize BFS queue with the original string
7 queue = deque([s])
8
9 # Keep track of visited strings to avoid cycles
10 visited: Set[str] = {s}
11
12 # Initialize result with the original string
13 smallest_string = s
14
15 # BFS to explore all possible string transformations
16 while queue:
17 current_string = queue.popleft()
18
19 # Update the smallest string if current is lexicographically smaller
20 if smallest_string > current_string:
21 smallest_string = current_string
22
23 # Operation 1: Add 'a' to all odd-indexed digits (modulo 10)
24 transformed_add = ''.join(
25 str((int(char) + a) % 10) if index % 2 == 1 else char
26 for index, char in enumerate(current_string)
27 )
28
29 # Operation 2: Rotate string by 'b' positions to the right
30 # Take last b characters and move them to the front
31 transformed_rotate = current_string[-b:] + current_string[:-b]
32
33 # Add both transformations to queue if not visited
34 for transformed in (transformed_add, transformed_rotate):
35 if transformed not in visited:
36 visited.add(transformed)
37 queue.append(transformed)
38
39 return smallest_string
40
1class Solution {
2 public String findLexSmallestString(String s, int a, int b) {
3 // Use BFS to explore all possible string transformations
4 Deque<String> queue = new ArrayDeque<>();
5 queue.offer(s);
6
7 // Track visited strings to avoid processing duplicates
8 Set<String> visited = new HashSet<>();
9 visited.add(s);
10
11 // Initialize the answer with the original string
12 String smallestString = s;
13 int length = s.length();
14
15 // BFS to explore all reachable strings
16 while (!queue.isEmpty()) {
17 String currentString = queue.poll();
18
19 // Update answer if current string is lexicographically smaller
20 if (smallestString.compareTo(currentString) > 0) {
21 smallestString = currentString;
22 }
23
24 // Operation 1: Add 'a' to all odd-indexed digits
25 char[] charArray = currentString.toCharArray();
26 for (int i = 1; i < length; i += 2) {
27 // Add 'a' to digit at odd index and take modulo 10
28 charArray[i] = (char) (((charArray[i] - '0' + a) % 10) + '0');
29 }
30 String additionResult = String.valueOf(charArray);
31
32 // Operation 2: Rotate string to the right by 'b' positions
33 String rotationResult = currentString.substring(length - b) +
34 currentString.substring(0, length - b);
35
36 // Add both transformed strings to queue if not visited
37 for (String transformedString : List.of(additionResult, rotationResult)) {
38 if (visited.add(transformedString)) {
39 queue.offer(transformedString);
40 }
41 }
42 }
43
44 return smallestString;
45 }
46}
47
1class Solution {
2public:
3 string findLexSmallestString(string s, int a, int b) {
4 // Use BFS to explore all possible string transformations
5 queue<string> bfsQueue{{s}};
6 unordered_set<string> visited{{s}};
7 string minString = s;
8 int stringLength = s.size();
9
10 while (!bfsQueue.empty()) {
11 string currentString = bfsQueue.front();
12 bfsQueue.pop();
13
14 // Update the lexicographically smallest string found so far
15 minString = min(minString, currentString);
16
17 // Operation 1: Add 'a' to all odd-indexed characters
18 string addResult = currentString;
19 for (int i = 1; i < stringLength; i += 2) {
20 // Convert char to digit, add 'a', apply modulo 10, convert back to char
21 addResult[i] = (addResult[i] - '0' + a) % 10 + '0';
22 }
23
24 // Operation 2: Rotate string to the right by 'b' positions
25 // Take last 'b' characters and concatenate with first 'n-b' characters
26 string rotateResult = currentString.substr(stringLength - b) +
27 currentString.substr(0, stringLength - b);
28
29 // Process both transformed strings
30 for (const auto& transformedString : {addResult, rotateResult}) {
31 // If this string hasn't been visited, add it to the queue
32 if (!visited.count(transformedString)) {
33 visited.insert(transformedString);
34 bfsQueue.emplace(transformedString);
35 }
36 }
37 }
38
39 return minString;
40 }
41};
42
1function findLexSmallestString(s: string, a: number, b: number): string {
2 // Use BFS to explore all possible string transformations
3 const bfsQueue: string[] = [s];
4 const visited: Set<string> = new Set([s]);
5 let minString: string = s;
6 const stringLength: number = s.length;
7
8 while (bfsQueue.length > 0) {
9 // Dequeue the current string to process
10 const currentString: string = bfsQueue.shift()!;
11
12 // Update the lexicographically smallest string found so far
13 if (currentString < minString) {
14 minString = currentString;
15 }
16
17 // Operation 1: Add 'a' to all odd-indexed characters
18 let addResult: string = "";
19 for (let i = 0; i < stringLength; i++) {
20 if (i % 2 === 1) {
21 // Convert char to digit, add 'a', apply modulo 10, convert back to char
22 const digit = (parseInt(currentString[i]) + a) % 10;
23 addResult += digit.toString();
24 } else {
25 // Keep even-indexed characters unchanged
26 addResult += currentString[i];
27 }
28 }
29
30 // Operation 2: Rotate string to the right by 'b' positions
31 // Take last 'b' characters and concatenate with first 'n-b' characters
32 const rotateResult: string = currentString.substring(stringLength - b) +
33 currentString.substring(0, stringLength - b);
34
35 // Process both transformed strings
36 const transformedStrings: string[] = [addResult, rotateResult];
37 for (const transformedString of transformedStrings) {
38 // If this string hasn't been visited, add it to the queue
39 if (!visited.has(transformedString)) {
40 visited.add(transformedString);
41 bfsQueue.push(transformedString);
42 }
43 }
44 }
45
46 return minString;
47}
48
Time and Space Complexity
Time Complexity: O(n * 10^n)
The algorithm uses BFS to explore all possible string transformations. In the worst case:
- Each digit position can have at most 10 different values (0-9)
- The string has
n
digits - The maximum number of unique states is bounded by
10^n
(all possible digit combinations) - For each state, we perform operations that take
O(n)
time:- Creating string
t1
with the add operation:O(n)
- Creating string
t2
with the rotate operation:O(n)
- String comparison for finding minimum:
O(n)
- Creating string
- Therefore, the overall time complexity is
O(n * 10^n)
Space Complexity: O(n * 10^n)
The space usage consists of:
- The queue
q
which can contain up to10^n
unique strings in the worst case - The visited set
vis
which stores all unique states encountered, up to10^n
strings - Each string has length
n
, so storing each string requiresO(n)
space - Additional temporary strings
t1
andt2
requireO(n)
space but don't affect the asymptotic complexity - Therefore, the total space complexity is
O(n * 10^n)
Note: In practice, the actual number of reachable states may be much smaller than 10^n
depending on the values of a
and b
, but the worst-case complexity remains as stated.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Rotation Direction or Formula
A common mistake is implementing the rotation operation incorrectly. Many developers confuse left rotation with right rotation or use the wrong slicing indices.
Pitfall Example:
# Wrong: This performs left rotation instead of right transformed_rotate = current_string[b:] + current_string[:b] # Wrong: Missing handling when b > len(s) transformed_rotate = current_string[-b:] + current_string[:-b] # Fails if b > len(s)
Solution:
# Correct right rotation with modulo to handle b > len(s)
b = b % len(current_string) # Handle cases where b exceeds string length
transformed_rotate = current_string[-b:] + current_string[:-b] if b > 0 else current_string
2. Not Handling Edge Cases for Rotation
When b = 0
or b
is a multiple of the string length, the rotation operation doesn't change the string, but the code might still add it to the queue unnecessarily.
Pitfall Example:
# Inefficient: Always performs rotation even when b = 0 transformed_rotate = current_string[-b:] + current_string[:-b]
Solution:
# Optimize by checking if rotation actually changes the string
b_effective = b % len(current_string)
if b_effective != 0:
transformed_rotate = current_string[-b_effective:] + current_string[:-b_effective]
if transformed_rotate not in visited:
visited.add(transformed_rotate)
queue.append(transformed_rotate)
3. Inefficient String Comparison for Finding Minimum
Comparing strings inside the BFS loop for every dequeued element can be inefficient, especially when the same string might be dequeued multiple times through different paths.
Pitfall Example:
# Inefficient: Updates smallest_string even if current is larger if smallest_string > current_string: smallest_string = current_string
Solution:
# Better approach: Track minimum only when discovering new strings while queue: current_string = queue.popleft() for transformed in (transformed_add, transformed_rotate): if transformed not in visited: visited.add(transformed) queue.append(transformed) # Update minimum only for newly discovered strings if smallest_string > transformed: smallest_string = transformed
4. Memory Issues with Large Input
For large strings or when operations generate many unique states, the visited set can consume significant memory.
Solution:
# Add early termination if we've explored too many states
MAX_STATES = 10000 # Adjust based on constraints
if len(visited) > MAX_STATES:
break # Early termination to prevent memory issues
5. Integer Overflow in Addition Operation
While Python handles large integers well, in other languages or when optimizing, not properly handling the modulo operation can cause issues.
Pitfall Example:
# Potential issue if not careful with order of operations
str((int(char) + a) % 10) # Make sure parentheses are correct
Solution:
# Clear and safe implementation
digit = int(char)
new_digit = (digit + a) % 10
result = str(new_digit)
A heap is a ...?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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
Want a Structured Path to Master System Design Too? Donβt Miss This!