220 lines
10 KiB
YAML
220 lines
10 KiB
YAML
title: Check If String Is Transformable With Substring Sort Operations
|
|
slug: check-if-string-is-transformable-with-substring-sort-operations
|
|
difficulty: hard
|
|
leetcode_id: 1585
|
|
leetcode_url: https://leetcode.com/problems/check-if-string-is-transformable-with-substring-sort-operations/
|
|
categories:
|
|
- strings
|
|
- sorting
|
|
patterns:
|
|
- greedy
|
|
|
|
function_signature: "def is_transformable(s: str, t: str) -> bool:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { s: "84532", t: "34852" }
|
|
expected: true
|
|
- input: { s: "34521", t: "23415" }
|
|
expected: true
|
|
- input: { s: "12345", t: "12435" }
|
|
expected: false
|
|
hidden:
|
|
- input: { s: "1", t: "1" }
|
|
expected: true
|
|
- input: { s: "12", t: "12" }
|
|
expected: true
|
|
- input: { s: "21", t: "12" }
|
|
expected: true
|
|
- input: { s: "12", t: "21" }
|
|
expected: false
|
|
- input: { s: "111", t: "111" }
|
|
expected: true
|
|
|
|
description: |
|
|
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 `s` and 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"`.
|
|
|
|
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.
|
|
|
|
constraints: |
|
|
- `s.length == t.length`
|
|
- `1 <= s.length <= 10^5`
|
|
- `s` and `t` consist of only digits.
|
|
|
|
examples:
|
|
- 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", then "84352" (from index 0 to 2) -> "34852".'
|
|
- input: 's = "34521", t = "23415"'
|
|
output: "true"
|
|
explanation: 'You can transform s into t using the following sort operations: "34521" -> "23451" -> "23415".'
|
|
- input: 's = "12345", t = "12435"'
|
|
output: "false"
|
|
explanation: "No sequence of sorting operations can transform s into t because we cannot move a larger digit to the left of a smaller digit."
|
|
|
|
explanation:
|
|
intuition: |
|
|
The key insight is understanding what the sorting operation **can** and **cannot** do.
|
|
|
|
When you sort a substring in ascending order, you're essentially **bubbling smaller characters to the left** within that substring. Think of it like bubble sort: a smaller digit can "pass through" larger digits to its left, one position at a time, by repeatedly sorting adjacent pairs.
|
|
|
|
However, the reverse is **impossible**: a larger digit **cannot** move to the left of a smaller digit. Why? Because any sorting operation would push the smaller digit back to the left of the larger one.
|
|
|
|
This gives us our fundamental rule: **a digit `d` can only move leftward in `s` if there is no smaller digit blocking its path**.
|
|
|
|
Think of it like a queue at each digit position. For each digit `0-9`, we track where instances of that digit appear in `s`. When building `t` character by character, we ask: "Can the next required digit from `s` move to this position?" It can move left if and only if no smaller digit stands in its way (i.e., appears at an earlier index that we haven't yet consumed).
|
|
|
|
approach: |
|
|
We solve this using a **Greedy Index Queue** approach:
|
|
|
|
**Step 1: Basic validation**
|
|
|
|
- First, verify `s` and `t` have the same character frequencies — if not, transformation is impossible
|
|
- This ensures we have the right "ingredients" to build `t`
|
|
|
|
|
|
|
|
**Step 2: Build position queues for each digit**
|
|
|
|
- Create 10 queues (one for each digit `0-9`)
|
|
- For each digit in `s`, append its index to the corresponding queue
|
|
- This gives us the positions where each digit appears, in left-to-right order
|
|
|
|
|
|
|
|
**Step 3: Process each character in `t`**
|
|
|
|
- For each character `c` in `t`, we need to "consume" one instance of `c` from `s`
|
|
- Get the leftmost unconsumed position of `c` from its queue (call it `pos`)
|
|
- **Critical check**: For all digits smaller than `c` (i.e., `0` to `c-1`), verify their queues are either empty or their front index is greater than `pos`
|
|
- If any smaller digit has an unconsumed position to the left of `pos`, return `false` — that smaller digit blocks `c` from moving left
|
|
- If the check passes, pop `pos` from `c`'s queue (mark it as consumed)
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- If we successfully process all characters in `t`, return `true`
|
|
|
|
|
|
|
|
The greedy approach works because we always consume the leftmost available instance of each digit. If that instance can legally move to its target position (no smaller blockers), we proceed. Otherwise, no sequence of operations can help.
|
|
|
|
common_pitfalls:
|
|
- title: Ignoring the Movement Constraint
|
|
description: |
|
|
A common mistake is thinking any digit can move anywhere as long as character frequencies match.
|
|
|
|
For example, with `s = "12345"` and `t = "12435"`, the frequencies match perfectly. But transforming `s` to `t` requires moving `4` to the left of `3`. Since `3 < 4`, the `3` will always sort to the left of `4` — the transformation is impossible.
|
|
|
|
Always check whether smaller digits block the required movements.
|
|
wrong_approach: "Only checking if s and t have the same character counts"
|
|
correct_approach: "Verify no smaller digit blocks each required leftward movement"
|
|
|
|
- title: Using Simulation Instead of Position Tracking
|
|
description: |
|
|
Trying to simulate the actual sorting operations leads to exponential complexity. There are infinitely many ways to choose substrings, and finding the right sequence (if it exists) is computationally infeasible for large inputs.
|
|
|
|
With `n = 10^5`, any approach worse than O(n) or O(n log n) will likely cause TLE.
|
|
wrong_approach: "BFS/DFS to explore all possible sorting sequences"
|
|
correct_approach: "Track digit positions and verify movement constraints in O(n)"
|
|
|
|
- title: Checking All Pairs Instead of Using Queues
|
|
description: |
|
|
A naive O(n^2) approach checks, for each position in `t`, whether any smaller digit in `s` blocks it by scanning the entire string.
|
|
|
|
Using queues for each digit reduces this to O(1) amortised per check — we only look at the front of each smaller digit's queue.
|
|
wrong_approach: "For each character, scan all of s to find blockers"
|
|
correct_approach: "Maintain queues of positions, check only queue fronts"
|
|
|
|
key_takeaways:
|
|
- "**Movement analysis**: When operations have constraints (like sorting), analyse what movements are possible vs impossible"
|
|
- "**Greedy validation**: Instead of simulating operations, verify that required movements don't violate constraints"
|
|
- "**Queue-based position tracking**: Storing indices in queues enables efficient leftmost-first processing"
|
|
- "**Digit-based problems**: With only 10 possible digits, maintaining 10 separate data structures is both simple and efficient"
|
|
|
|
time_complexity: "O(n). We iterate through `t` once, and each digit position is added to and removed from a queue exactly once. Checking smaller digits is O(10) = O(1) per character."
|
|
space_complexity: "O(n). We store all indices of `s` across the 10 queues, which totals `n` indices."
|
|
|
|
solutions:
|
|
- approach_name: Greedy with Index Queues
|
|
is_optimal: true
|
|
code: |
|
|
from collections import deque, Counter
|
|
|
|
def is_transformable(s: str, t: str) -> bool:
|
|
# Basic validation: must have same character frequencies
|
|
if Counter(s) != Counter(t):
|
|
return False
|
|
|
|
# Build queues of positions for each digit (0-9)
|
|
# pos[d] stores indices where digit d appears in s
|
|
pos = [deque() for _ in range(10)]
|
|
for i, c in enumerate(s):
|
|
pos[int(c)].append(i)
|
|
|
|
# Process each character in t
|
|
for c in t:
|
|
d = int(c)
|
|
# Get the leftmost position of digit d in s
|
|
idx = pos[d][0]
|
|
|
|
# Check if any smaller digit blocks this position
|
|
# A smaller digit blocks if it appears to the left of idx
|
|
for smaller in range(d):
|
|
if pos[smaller] and pos[smaller][0] < idx:
|
|
# Smaller digit at earlier index blocks movement
|
|
return False
|
|
|
|
# No blockers - consume this position
|
|
pos[d].popleft()
|
|
|
|
return True
|
|
explanation: |
|
|
**Time Complexity:** O(n) — We iterate through both strings once. Each index is enqueued and dequeued exactly once, and we check at most 10 smaller digits per character.
|
|
|
|
**Space Complexity:** O(n) — The 10 queues collectively store all n indices from string s.
|
|
|
|
The algorithm validates that each character in `t` can be formed by checking whether any smaller digit would block the required leftward movement. By using queues, we efficiently track the leftmost available position for each digit.
|
|
|
|
- approach_name: Brute Force (Conceptual)
|
|
is_optimal: false
|
|
code: |
|
|
def is_transformable_brute(s: str, t: str) -> bool:
|
|
# NOTE: This approach is conceptual and will TLE on large inputs.
|
|
# It simulates trying all possible sorting operations via BFS.
|
|
from collections import deque
|
|
|
|
if sorted(s) != sorted(t):
|
|
return False
|
|
|
|
visited = {s}
|
|
queue = deque([s])
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
if current == t:
|
|
return True
|
|
|
|
# Try all possible substring sorts (exponential!)
|
|
for i in range(len(current)):
|
|
for j in range(i + 1, len(current) + 1):
|
|
# Sort substring [i:j]
|
|
new_s = current[:i] + ''.join(sorted(current[i:j])) + current[j:]
|
|
if new_s not in visited:
|
|
visited.add(new_s)
|
|
queue.append(new_s)
|
|
|
|
return False
|
|
explanation: |
|
|
**Time Complexity:** O(n! * n^2) in the worst case — exponential number of states, each requiring O(n^2) substring operations.
|
|
|
|
**Space Complexity:** O(n! * n) — storing all visited states.
|
|
|
|
This BFS approach explores all possible sorting operations. While correct, it's astronomically slow for any non-trivial input size. It's included to illustrate why the greedy approach is essential — we need to reason about what's possible rather than exhaustively searching.
|