Scramble String is a recursion-and-memoization classic. The definition is self-referential — a string is scrambled by splitting it and scrambling each half — so the solution is naturally recursive, and the overlapping subproblems beg for a memo.
Problem. A string can be scrambled like this: split it into two non-empty halves, then optionally
swap the halves, and recursively scramble each half the same way. Given two equal-length strings s1 and
s2, return true if s2 is a scramble of s1.
Example: s1 = "great", s2 = "rgeat" → true. Split s1 after 2 letters into "gr" and "eat",
scramble "gr" into "rg" by swapping its two letters, and leave "eat" alone: "rg" + "eat" = "rgeat".
The slow way first
The brute force is to literally generate every scramble of s1 and see if s2 is among them. At each level you pick a split point and choose whether to swap, so the number of scrambles explodes — this is exponential and quickly hopeless even for short strings.
The better question: instead of building all scrambles, can I check membership directly? If s2 is a scramble of s1, then there must exist a split where the two halves line up — either straight across or swapped. That turns "generate everything" into "search for one good split."
The idea: try every split, straight or swapped
For some split index i, s1 is scramble-equal to s2 when either:
- no swap —
s1[:i] ~ s2[:i]ands1[i:] ~ s2[i:], or - swapped —
s1[:i] ~ s2[n-i:]ands1[i:] ~ s2[:n-i].
The ~ is the same scramble relation, applied recursively to smaller pieces. Two base cases stop the recursion: equal strings always match, and strings with different letter counts never match (a cheap pruning check). Because the same (s1, s2) pair comes up again and again, we memoize every result.
The key insight: a swap reverses which half of s2 each half of s1 must match, which is why the swapped branch compares s1's left against s2's right end.
Walk through it
Step through the animation. The top row is s1, the bottom row is s2, and the split pointer marks where we cut s1. The cut at i = 1 fails fast. Sliding it to i = 2 gives halves "gr" and "eat": the swapped branch matches "gr" to "rg" (just g~g and r~r), and the right halves "eat" and "eat" are already equal. One split worked, so the answer is True.
Pseudocode
function scramble(s1, s2):
if s1 == s2: return True # base: identical
if sorted(s1) != sorted(s2): # base: different letters
return False
for each split i from 1 to len-1:
# straight pairing
if scramble(s1[:i], s2[:i]) and scramble(s1[i:], s2[i:]):
return True
# swapped pairing
if scramble(s1[:i], s2[n-i:]) and scramble(s1[i:], s2[:n-i]):
return True
return False
# memoize every (s1, s2) result so it is computed onceThe Python solution
def is_scramble(s1, s2, memo={}):
if s1 == s2:
return True
if sorted(s1) != sorted(s2):
return False
n = len(s1)
for i in range(1, n):
if (is_scramble(s1[:i], s2[:i]) and
is_scramble(s1[i:], s2[i:])):
return True
if (is_scramble(s1[:i], s2[n - i:]) and
is_scramble(s1[i:], s2[:n - i])):
return True
return Falseif s1 == s2is the success base case — identical strings are trivially scrambles.if sorted(s1) != sorted(s2)prunes hard: different letter multisets can never match, so we bail before any recursion.- The loop tries every split point
ifrom1ton-1. - The no-swap check pairs left-with-left and right-with-right.
- The swapped check pairs
s1's left withs2's lasticharacters, ands1's right withs2's firstn-icharacters. - The first split that satisfies either pairing returns
True; if none do, the strings are not scrambles. Amemoon(s1, s2)keeps each pair from being recomputed.
Complexity
| Case | Time | Notes |
|---|---|---|
| Brute force (generate scrambles) | exponential (moderate) | all split/swap choices |
| Memoized recursion | O(n^4) (moderate) | O(n^3) distinct (s1, s2) pairs, O(n) work each |
O(n^3) (moderate)A subproblem is fixed by a start index in s1, a start index in s2, and a length — that is O(n^3) distinct states, and each does O(n) work over the split points. Memoization is what turns the exponential search into a polynomial one.
When this pattern shows up
When a problem is defined recursively in terms of itself on smaller pieces ("a valid X is two valid Xs joined / swapped"), model it as a recursion over splits and add a memo keyed by the exact subproblem. Interval DP, parenthesization, and many string-partition problems share this shape.
Do not forget the swapped branch indices. When the halves are swapped, s1's left half must match the
tail of s2 (s2[n-i:]), not its head. Mixing these up is the most common bug, and it silently
returns wrong answers rather than crashing.
Practice
For s1 = 'great', s2 = 'rgeat', the split i = 2 gives halves 'gr' and 'eat'. Which branch makes 'gr' match the first two letters of s2, and why?
1. What are the two base cases of the recursion?
2. In the swapped branch, what does s1's left half get compared against?
3. Why does this problem need memoization?
4. What is the purpose of the sorted(s1) != sorted(s2) check?