class Solution:
def isScramble(self, s1: str, s2: str) -> bool:
from functools import lru_cache
@lru_cache(maxsize=None)
def dfs(a, b):
if a == b:
return True
if sorted(a) != sorted(b):
return False
n = len(a)
for i in range(1, n):
# Without swap
if dfs(a[:i], b[:i]) and dfs(a[i:], b[i:]):
return True
# With swap
if dfs(a[:i], b[-i:]) and dfs(a[i:], b[:-i]):
return True
return False
return dfs(s1, s2)
class Solution {
public:
unordered_map<string, bool> memo;
bool isScramble(string s1, string s2) {
string key = s1 + " " + s2;
if (memo.count(key)) return memo[key];
if (s1 == s2) return memo[key] = true;
string t1 = s1, t2 = s2;
sort(t1.begin(), t1.end());
sort(t2.begin(), t2.end());
if (t1 != t2) return memo[key] = false;
int n = s1.size();
for (int i = 1; i < n; ++i) {
// No swap
if (isScramble(s1.substr(0, i), s2.substr(0, i)) &&
isScramble(s1.substr(i), s2.substr(i)))
return memo[key] = true;
// Swap
if (isScramble(s1.substr(0, i), s2.substr(n-i)) &&
isScramble(s1.substr(i), s2.substr(0, n-i)))
return memo[key] = true;
}
return memo[key] = false;
}
};
import java.util.*;
class Solution {
Map<String, Boolean> memo = new HashMap<>();
public boolean isScramble(String s1, String s2) {
String key = s1 + " " + s2;
if (memo.containsKey(key)) return memo.get(key);
if (s1.equals(s2)) {
memo.put(key, true);
return true;
}
char[] arr1 = s1.toCharArray();
char[] arr2 = s2.toCharArray();
Arrays.sort(arr1);
Arrays.sort(arr2);
if (!Arrays.equals(arr1, arr2)) {
memo.put(key, false);
return false;
}
int n = s1.length();
for (int i = 1; i < n; i++) {
// No swap
if (isScramble(s1.substring(0, i), s2.substring(0, i)) &&
isScramble(s1.substring(i), s2.substring(i))) {
memo.put(key, true);
return true;
}
// Swap
if (isScramble(s1.substring(0, i), s2.substring(n-i)) &&
isScramble(s1.substring(i), s2.substring(0, n-i))) {
memo.put(key, true);
return true;
}
}
memo.put(key, false);
return false;
}
}
var isScramble = function(s1, s2) {
const memo = new Map();
function dfs(a, b) {
const key = a + "#" + b;
if (memo.has(key)) return memo.get(key);
if (a === b) {
memo.set(key, true);
return true;
}
if ([...a].sort().join('') !== [...b].sort().join('')) {
memo.set(key, false);
return false;
}
const n = a.length;
for (let i = 1; i < n; i++) {
// No swap
if (dfs(a.slice(0, i), b.slice(0, i)) &&
dfs(a.slice(i), b.slice(i))) {
memo.set(key, true);
return true;
}
// Swap
if (dfs(a.slice(0, i), b.slice(n-i)) &&
dfs(a.slice(i), b.slice(0, n-i))) {
memo.set(key, true);
return true;
}
}
memo.set(key, false);
return false;
}
return dfs(s1, s2);
};
The Scramble String problem asks: given two strings s1
and s2
of the same length, determine if s2
is a scrambled version of s1
.
A string is a scramble of another string if you can recursively partition it into two non-empty substrings, swap them (or not), and repeat the process on the substrings. At each step, you can choose to swap or not swap the two parts. The process continues recursively for each substring.
Constraints:
s1
and s2
are of the same length.true
if s2
is a scrambled string of s1
, and false
otherwise.
At first glance, the problem seems to ask for a check on whether the two strings can be transformed into each other by recursively swapping substrings. A brute-force approach would be to try all possible partitions and swaps, but this quickly becomes computationally expensive due to the exponential number of possibilities.
The challenge is to efficiently check all possible ways to partition and swap substrings, and to avoid redundant computation. We notice that many subproblems repeat themselves, so memoization (caching) can save us from recalculating the same scenario multiple times.
The key insight is that if two substrings are scrambled versions of each other, then their character counts must match. This allows us to prune impossible cases early, before recursing further.
We solve the problem using recursion with memoization (top-down dynamic programming). Here are the steps:
false
immediately.
s1
matches the first part of s2
and the second part of s1
matches the second part of s2
.
s1
matches the second part of s2
and vice versa.
true
.
This approach ensures that we only explore feasible paths and avoid repeated calculations, making the algorithm much more efficient.
Let's consider s1 = "great"
and s2 = "rgeat"
.
s1
: "g" | "reat"
s2
: "r" | "geat"
s1
: "gr" | "eat"
s2
: "rg" | "eat"
The recursion explores all valid splits and swaps, but thanks to memoization and pruning, it avoids redundant or impossible paths.
Brute-force approach: Without memoization, the number of ways to partition and swap grows exponentially with the string length, leading to a time complexity of O(4^n)
for strings of length n
.
Optimized approach (with memoization): Each unique pair of substrings is computed at most once. There are O(n^4)
possible pairs (since there are O(n^2)
substrings for each string), and each computation involves O(n)
work (for splitting and sorting/counting). Thus, the overall time complexity is O(n^4)
.
Space complexity: The memoization table stores results for O(n^4)
substring pairs, so space complexity is also O(n^4)
.
In practice, pruning with character counts and memoization makes the solution efficient for reasonable string lengths.
The Scramble String problem is a classic example of recursive problem-solving with memoization. By breaking the problem into smaller subproblems, pruning impossible cases early, and caching results, we can efficiently determine whether two strings are scrambled versions of each other. The elegance of this solution lies in its combination of recursion, memoization, and early pruning, which together transform an exponential brute-force search into a tractable algorithm.