Check If String Is Transformable With Substring Sort Operations
HardPrompt
Given two strings s and t, transform string s into string t using the following operation any number of times:
- Choose a non-empty substring in
sand sort it in place so the characters are in ascending order.- For example, applying the operation on the underlined substring in
"14234"results in"12344".
- For example, applying the operation on the underlined substring in
Return true if it is possible to transform s into t. Otherwise, return false.
A substring is a contiguous sequence of characters within a string.
Example 1:
Input: s = "84532", t = "34852"
Output: true
Explanation: You can transform s into t using the following sort operations:
"84532" (from index 2 to 3) -> "84352"
"84352" (from index 0 to 2) -> "34852"Example 2:
Input: s = "34521", t = "23415"
Output: true
Explanation: You can transform s into t using the following sort operations:
"34521" -> "23451"
"23451" -> "23415"Example 3:
Input: s = "12345", t = "12435"
Output: false
Constraints:
s.length == t.length1 <= s.length <= 105sandtconsist of only digits.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach refines the greedy strategy by optimizing the search for characters and their blockers. Instead of repeatedly scanning the string s, we pre-process it to store the indices of each character. This allows us to find the position of the next available character and check for blocking characters in constant time (per digit), leading to a much faster overall algorithm.
Algorithm
- First, perform an anagram check. If
sandtdon't have the same character counts, returnfalse. - Pre-process
sto store the indices of each digit. An array of queues or lists,positions, is suitable, wherepositions[d]holds a sorted list of indices where digitdappears ins. - Iterate through
tfromi = 0ton-1. Letcbe the charactert[i]. - The greedy strategy dictates we must use the leftmost available occurrence of
cfroms. We can get its index,j, by looking at the front of the queuepositions[c - '0']. - Now, check if this move is valid. For every digit
dthat is smaller thanc(i.e.,d < c - '0'), check if its leftmost available occurrence is at an index smaller thanj. If!positions[d].isEmpty()andpositions[d].peek() < j, it meanscis blocked by a smaller digitd. Returnfalse. - If
cis not blocked by any smaller digit, the move is valid. We consume this occurrence ofcby removing its index from the front of its queue (e.g.,positions[c - '0'].poll()). - If the loop completes successfully for all characters of
t, returntrue.
Walkthrough
The core logic remains the same as the naive approach: a character c at index j in s can be moved to the current leftmost position if and only if there are no smaller available characters at indices less than j. The key to optimization is to avoid the O(N) scans in each step.
We can achieve this by pre-computing the locations of all digits. We use an array of queues, say positions, where positions[d] stores all indices of digit d in s in increasing order.
We then iterate through t. For each t[i], we identify the character c = t[i]. The greedy choice is to use the first available c from s, whose index j is at the front of positions[c - '0']. To check if this is a valid move, we must ensure that for any digit d < c - '0', the first available d is not to the left of j. We can quickly check this by peeking at the front of positions[d] for all d < c - '0'. If we find any such blocking character, we return false. Otherwise, the move is valid, and we remove j from its queue (positions[c - '0'].poll()) to mark it as used.
This pre-computation allows all lookups and checks inside the main loop to be very fast, reducing the complexity from quadratic to linear.
import java.util.ArrayList;import java.util.LinkedList;import java.util.List;import java.util.Queue; class Solution { public boolean isTransformable(String s, String t) { // Anagram check int[] counts = new int[10]; for (char c : s.toCharArray()) { counts[c - '0']++; } for (char c : t.toCharArray()) { counts[c - '0']--; } for (int count : counts) { if (count != 0) { return false; } } // Pre-process s to store indices of each digit List<Queue<Integer>> positions = new ArrayList<>(); for (int i = 0; i < 10; i++) { positions.add(new LinkedList<>()); } for (int i = 0; i < s.length(); i++) { positions.get(s.charAt(i) - '0').add(i); } // Greedily build t for (int i = 0; i < t.length(); i++) { int digit = t.charAt(i) - '0'; int pos = positions.get(digit).peek(); // Check for smaller blocking digits for (int smallerDigit = 0; smallerDigit < digit; smallerDigit++) { if (!positions.get(smallerDigit).isEmpty() && positions.get(smallerDigit).peek() < pos) { return false; } } // Consume the digit positions.get(digit).poll(); } return true; }}Complexity
Time
O(N). The anagram check takes O(N). Populating the `positions` data structure takes O(N). The main loop iterates N times. Inside this loop, we have another loop that runs at most 10 times (for digits 0-9). Thus, the work inside the main loop is O(10 * N) = O(N). The total time complexity is linear.
Space
O(N), where N is the length of the strings. The `positions` data structure stores a total of N indices. The character count array takes constant space O(1) as there are only 10 digits.
Trade-offs
Pros
Highly efficient with a linear time complexity, making it suitable for large inputs.
It is an optimal solution for this problem.
Cons
Requires more complex data structures (array of lists/queues) compared to the naive approach.
Uses more space to store all indices from the original string.
This approach simulates the process of building the target string t from s in a greedy manner. We iterate through t from left to right. For each character t[i], we find its earliest available occurrence in s and check if it can be moved to the current position. A move is possible only if there are no smaller, un-used characters to its left in s that would block it.
Algorithm
- First, check if
sandtare anagrams. If their character counts don't match, it's impossible to transformstot, so returnfalse. - Initialize a boolean array
usedof sizen(the length of the strings) to allfalse. This array will keep track of which characters fromshave been used. - Iterate through the target string
tfrom left to right, with indexifrom0ton-1. - For each character
t[i], find the first available matching character ins. This means finding the smallest indexjsuch thats[j] == t[i]andused[j]isfalse. - Once this character
s[j]is found, check if it's possible to move it to the current position. A character can be moved left past any larger or equal characters. It is only blocked by smaller characters. Therefore, we must check if there are any unused characters to the left ofs[j]that are smaller than it. Iterate fromk = 0toj-1. If we find an indexkwhereused[k]isfalseands[k] < s[j], it meanss[j]is blocked. The transformation is impossible, so returnfalse. - If
s[j]is not blocked, it means we can use it to formt[i]. Mark it as used by settingused[j] = true. - If the loop completes for all characters in
t, it means a valid transformation sequence exists. Returntrue.
Walkthrough
The fundamental observation is that the sorting operation allows smaller characters to move to the left past larger characters, but a character can never move to the left of a character that is smaller than it. This establishes a relative ordering constraint.
This approach greedily constructs t one character at a time. For t[0], we need to find a character in s that can become the first character. Then for t[1], we find a character from the remaining ones in s that can become the second, and so on. The greedy choice is to always use the leftmost available character from s that matches the required character t[i].
To implement this, we first verify that s and t are anagrams. Then, we iterate through t. For each t[i], we scan s to find the first unused character s[j] that equals t[i]. To check if this s[j] can be moved into position, we must ensure no smaller unused character s[k] exists at an index k < j. If such a blocking character exists, the transformation is impossible. Otherwise, we mark s[j] as used and continue to the next character of t.
import java.util.Arrays; class Solution { public boolean isTransformable(String s, String t) { // Anagram check int[] counts = new int[10]; for (char c : s.toCharArray()) { counts[c - '0']++; } for (char c : t.toCharArray()) { counts[c - '0']--; } for (int count : counts) { if (count != 0) { return false; } } int n = s.length(); boolean[] used = new boolean[n]; for (int i = 0; i < n; i++) { char targetChar = t.charAt(i); int found_j = -1; // Find the leftmost unused character in s that matches targetChar for (int j = 0; j < n; j++) { if (!used[j] && s.charAt(j) == targetChar) { found_j = j; break; } } // Check for smaller characters to the left of found_j for (int k = 0; k < found_j; k++) { if (!used[k] && s.charAt(k) < targetChar) { return false; // Blocked by a smaller character } } used[found_j] = true; } return true; }}Complexity
Time
O(N^2), where N is the length of the strings. The outer loop runs N times (for each character in `t`). Inside, we have two nested loops that, in the worst case, scan through `s`. This results in a quadratic time complexity.
Space
O(N), where N is the length of the string. This is for the `used` boolean array to track which characters have been consumed.
Trade-offs
Pros
The logic is straightforward and directly models the constraints of the problem.
It's relatively easy to implement without complex data structures.
Cons
The time complexity is quadratic, which can be too slow for large inputs (like N=10^5).
Solutions
Solution
class Solution {public boolean isTransformable(String s, String t) { Deque<Integer>[] pos = new Deque[10]; Arrays.setAll(pos, k->new ArrayDeque<>()); for (int i = 0; i < s.length(); ++i) { pos[s.charAt(i) - '0'].offer(i); } for (int i = 0; i < t.length(); ++i) { int x = t.charAt(i) - '0'; if (pos[x].isEmpty()) { return false; } for (int j = 0; j < x; ++j) { if (!pos[j].isEmpty() && pos[j].peek() < pos[x].peek()) { return false; } } pos[x].poll(); } return true; }}class Solution {public: bool isTransformable(string s, string t) { queue<int> pos[10]; for (int i = 0; i < s.size(); ++i) { pos[s[i] - '0'].push(i); } for (char &c : t) { int x = c - '0'; if (pos[x].empty()) { return false; } for (int j = 0; j < x; ++j) { if (!pos[j].empty() && pos[j].front() < pos[x].front()) { return false; } } pos[x].pop(); } return true; }};class Solution: def isTransformable(self, s: str, t: str) -> bool: pos = defaultdict(deque) for i, c in enumerate(s): pos[int(c)]. append(i) for c in t: x = int(c) if not pos[x] or any(pos[i] and pos[i][0] < pos[x][0] for i in range(x)): return False pos[x]. popleft() return TrueVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.