String Permutations
Generate string arrangements without duplicates, step to the next permutation, jump to the k-th one, and find anagram windows with a count window
SPACED REPETITION · 18 practice questions
Make this lesson stick.
Try 3 questions now. No account needed. Sample answers aren't saved.
or sign in to practice all 18Forty-five answers, ten million calls
List every distinct rearrangement of aaaaaaaabb: eight a's and two b's. The standard permutation template treats the ten letters as ten different objects. It makes 9,864,101 recursive calls and outputs 3,628,800 strings. Only 45 of them are different. Each one comes out 80,640 times. A set at the end cleans up the output, but it does not give you back the ten million calls. The generator you will build in this lesson makes 219.
Now a different request. Does a 10,000-letter string s2 contain some rearrangement of a 10-letter string s1? Generating up to 3,628,800 rearrangements of s1 (exactly that many when its letters are distinct) and searching for each one is hopeless. And you never needed them. A slice of s2 is a rearrangement of s1 exactly when the two have the same letter counts, and there are only 9,991 slices of length 10 to check.
Both fixes rest on one idea: a string's arrangements are determined by its letter counts. Work with the counts: draw letters from them, count arrangements with them, compare them. Only list an arrangement when you actually have to output it.
| # | Smell | Move | Signature problem |
|---|---|---|---|
| 0 | "How many arrangements…?" | Count them, don't list them | warm-up |
| 1 | Your output repeats strings | Draw from a letter bag | Permutations II |
| 2 | "The next arrangement", in place | Pivot, successor, reverse | Next Permutation |
| 3 | "The k-th arrangement" | Skip whole blocks | Permutation Sequence |
| 4 | "Does some rearrangement appear in…?" | Slide a count window | Permutation in String |
This lesson builds on Permutations & Combinations: the choose/explore/unchoose template, the used array, the Counter version of Permutations II, and why n distinct items have n! orders. Advanced Recursion & Backtracking has the call-stack picture. From Foundation: Arrays & Strings you need count vectors, the in-place reverse helper, and the habit of building a string in a list and joining it once.
Warm-up: count before you generate
With n distinct letters there are n! arrangements. abc has 3! = 6.
Repeated letters change the count. Put temporary labels on the copies, as in a₁ a₂ b. The labelled letters are distinct, so there are 3! = 6 labelled arrangements. Now erase the labels. a₁ a₂ b and a₂ a₁ b both become aab. In general, every plain string comes from exactly c₁! × c₂! × … × c_k! labelled ones, where c₁, …, c_k are the letter counts: that is the number of ways to shuffle the labels among the positions of identical letters. So divide:
distinct arrangements = n! / (c₁! × c₂! × … × c_k!)
This is the multinomial coefficient. Call it M; the rest of the lesson uses that name.
| string | n | letter counts | M |
|---|---|---|---|
abc |
3 | 1, 1, 1 | 6 |
aab |
3 | 2, 1 | 3 |
aabb |
4 | 2, 2 | 6 |
aaaaaaaabb |
10 | 8, 2 | 45 |
mississippi |
11 | 4, 4, 2, 1 | 34,650 |
Every count divides, not just the first repeated one. Dividing aabb's 24 by 2! alone gives 12, which double-counts. The b's need their own 2!.
Predict first: how many distinct strings can you make by rearranging banana?
Check your answer
60. Six letters: a three times, n twice, b once. 6! / (3! × 2! × 1!) = 720 / 12 = 60. Forgetting the n's gives 720 / 6 = 120.
Why count first
Listing M strings of length n writes n × M characters. No generator can do less, so any "return them all" solution needs at least n·M steps. The count tells you whether listing is even possible. Python needs a few seconds to list the 10! = 3,628,800 arrangements of ten distinct letters. With twelve letters, 12! = 479,001,600, and listing them is out of reach. That is why problems that ask for all of them keep inputs tiny: LeetCode 46: Permutations allows at most 6 numbers, and LeetCode 47: Permutations II at most 8.
And 0! = 1: the empty string has exactly one arrangement, the empty one. Keep that in mind when you decide what your generator returns for "".
Your turn: the 30 distinct arrangements of aabbc are listed in dictionary order. How many start with a, and how many with c? Try to get there without writing all 30.
Check your answer
Fix the first letter and count what is left. Starting with a leaves abbc: 4! / 2! = 12. Starting with c leaves aabb: 4! / (2! × 2!) = 6. By symmetry b also leads 12, and 12 + 12 + 6 = 30.
There is a shortcut: each letter leads a share of the arrangements equal to its share of the letters. c is 1 of 5 letters, so it leads 30 × 1/5 = 6. a is 2 of 5, so it leads 12. Move 3 is built on exactly this block count.
Move 1: Draw from a letter bag
Smell: the problem says "return all unique permutations", the input can repeat letters, and your output contains the same string several times. Or you notice you're about to reach for a set to clean up at the end.
The echo in the tree
Predict first: run the plain position-based template (no duplicate handling) on aaaaaaaabb. How many strings does it output, and how many of those are aaaaaaaabb itself?
Check your answer
It outputs 10! = 3,628,800 strings, one per ordering of the ten positions. aaaaaaaabb appears 8! × 2! = 80,640 times: once for every way to shuffle the eight a-labels among themselves and the two b-labels among themselves. Every other distinct string also appears 80,640 times, and 3,628,800 / 80,640 = 45.
Here is the slow version: the position template, with a set at the end.
def distinct_perms_slow(s):
n = len(s)
used = [False] * n
path, out = [], set()
def build():
if len(path) == n:
out.add("".join(path))
return
for i in range(n): # choose a POSITION of s
if used[i]:
continue
used[i] = True
path.append(s[i])
build()
path.pop()
used[i] = False
build()
return sorted(out)
Watch what it repeats on aab. At the root it tries position 0 (an a) and position 1 (also an a), and the two subtrees below them are identical:
positions a₁ a₂ b: 6 leaves letters {a:2, b:1}: 3 leaves
a₁ ─ a₂ ─ b aab a ─ a ─ b aab
└ b ── a₂ aba └ b ─ a aba
a₂ ─ a₁ ─ b aab (echo) b ─ a ─ a baa
└ b ── a₁ aba (echo)
b ── a₁ ─ a₂ baa
└ a₂ ─ a₁ baa (echo)
The recursion tells copies apart even though the output can't. The fix is to choose letters, not positions: at each step, try each distinct letter once.
Choose letters, not positions
Keep a bag: a count of how many copies of each letter are still unused. This is the count vector from Foundation, stored in a Counter. If you met the Counter version of Permutations II in Permutations & Combinations, this is the same generator. For strings it is the natural default, and the rest of this lesson builds on it. Drawing a letter decrements its count, and putting it back increments it.
from collections import Counter
def distinct_perms(s):
bag = Counter(s) # letter -> copies not yet placed
letters = sorted(bag) # each distinct letter once, in order
n = len(s)
path, out = [], []
def build():
if len(path) == n:
out.append("".join(path))
return
for ch in letters:
if bag[ch] == 0:
continue
bag[ch] -= 1 # choose: draw one copy of ch
path.append(ch)
build() # explore
path.pop() # unchoose: put it back
bag[ch] += 1
build()
return out
print(distinct_perms("aab")) # ['aab', 'aba', 'baa']
Every call to build on aab, in order:
| call | path | bag | what happens |
|---|---|---|---|
| 1 | (empty) | a:2, b:1 | try a, b |
| 2 | a |
a:1, b:1 | try a, b |
| 3 | aa |
a:0, b:1 | try b |
| 4 | aab |
a:0, b:0 | output aab |
| 5 | ab |
a:1, b:0 | try a |
| 6 | aba |
a:0, b:0 | output aba |
| 7 | b |
a:2, b:0 | try a |
| 8 | ba |
a:1, b:0 | try a |
| 9 | baa |
a:0, b:0 | output baa |
Nine calls, three outputs, no echoes. The position template makes 16 calls on the same input.
Why it works
The invariant: before every call, bag holds exactly the letters of s that are not in path. Each draw moves one letter from the bag to the path, and each undo moves it back.
- No duplicates. Two sibling calls put different letters at the same position. So every string under one sibling differs from every string under the other at that position. Any two leaves split at some call, so any two outputs differ.
- Nothing missing. Take any distinct arrangement. At each position its next letter is still in the bag, because the bag is exactly what hasn't been placed yet. So the generator can follow that arrangement all the way down.
- Sorted for free. Letters are tried in sorted order at every level, so the output comes out in dictionary order.
What it costs
Let k be the number of distinct letters. While the path is incomplete, the bag is not empty, so every call either outputs a string or has at least one child. Calls at the same depth hold different prefixes, and each prefix leads to at least one output of its own. So each depth has at most M calls, and there are at most n·M + 1 calls in total.
Each call scans k letters, and each output costs O(n) to join. That gives O(k·n·M) time. For a fixed alphabet such as 26 lowercase letters, k ≤ 26 and this is O(n·M), the same as the size of the output, so asymptotically you can't do better. Extra space besides the output is O(n + k): the path and the recursion stack are n deep, and the bag has k entries.
On aaaaaaaabb the difference is dramatic:
generator on aaaaaaaabb |
calls | loop iterations |
|---|---|---|
| positions, then a set | 9,864,101 | 62,353,010 |
| letter bag | 219 | 348 |
Traps
⚠️ Append a snapshot, not the list. out.append(path) stores a reference to the one path list. Backtracking empties that list, so on abc you get [[], [], [], [], [], []]. "".join(path) builds a new string. For list outputs, use path[:].
⚠️ Every draw needs its undo. Delete bag[ch] += 1 and abc returns only ['abc']. The letters drawn for the first string never go back, so every later branch finds an empty bag.
Base case. For full arrangements, len(path) == n and "the bag is empty" are the same test. They differ once you stop early (length-k arrangements, below). There, only the length test is right.
Strings or lists? Passing prefix + ch down the recursion instead of path copies the prefix at every call. In practice this matters less than you might fear, because most calls sit near the leaves. With n distinct letters there are n!/(n − d)! calls at depth d, about e·n! ≈ 2.72·n! in all. For eight letters, 80,640 of the 109,601 calls are on the last two levels, and a leaf pays O(n) to join anyway. In CPython the two versions ran within a few percent of each other, and the concatenating one was slightly faster on nine and ten distinct letters. Use the list-and-join habit because its cost is predictable, not because concatenation is a disaster here.
If you index positions: the twin rule
Permutations & Combinations dedupes the position template differently. It sorts the letters, then skips a letter when an identical letter to its left is still unused:
def twin_rule_perms(s):
chars = sorted(s) # twins become neighbours
n = len(chars)
used = [False] * n
path, out = [], []
def build():
if len(path) == n:
out.append("".join(path))
return
for i in range(n):
if used[i]:
continue
if i > 0 and chars[i] == chars[i - 1] and not used[i - 1]:
continue # my left twin hasn't gone yet
used[i] = True
path.append(chars[i])
build()
path.pop()
used[i] = False
build()
return out
Read the skip as a queue rule: identical letters leave the bag left to right. Only one labelling of each string survives, the one where a₁ comes before a₂ comes before a₃. So each distinct string appears once. The sort is essential, because the rule only compares neighbours.
Plot twist: flip the condition to and used[i - 1]. The output is still correct: every distinct string appears exactly once, and both versions match a brute-force reference on 1,500 random strings. The flipped rule forces twins to leave right to left instead, which is also exactly one labelling per string. What changes is when the dead branches die:
generator on aaaaaaaabb |
calls | loop iterations |
|---|---|---|
twin rule, used[i - 1] |
94,070 | 940,250 |
twin rule, not used[i - 1] |
219 | 1,740 |
| letter bag | 219 | 348 |
With not used[i - 1], a wrong twin is refused at the first level where it is offered, so the echo subtree is never entered. With used[i - 1], the generator happily places a₁ first, walks deep into that subtree, and only discovers near the leaves that a₂ can never be placed. Both are correct; one prunes at the top and the other at the bottom. The letter bag makes the same calls as the good twin rule but scans only k letters per call instead of all n positions, so the twin rule costs O(n²·M) where the bag costs O(k·n·M).
If you swap: one set per level
The swap generator fixes position start by swapping each candidate into it, recursing on start + 1, and swapping back. The twin rule does not carry over, because swaps scramble the suffix and identical letters stop being neighbours. Sort aabc, add "skip if i > start and chars[i] == chars[i - 1]", and you get 14 strings, with caab and caba twice each. The fix is to remember which letters you have already placed at this position:
def swap_perms(s):
chars = list(s)
out = []
def build(start):
if start == len(chars):
out.append("".join(chars))
return
tried = set() # letters already placed at `start`
for i in range(start, len(chars)):
if chars[i] in tried:
continue
tried.add(chars[i])
chars[start], chars[i] = chars[i], chars[start]
build(start + 1)
chars[start], chars[i] = chars[i], chars[start] # swap back
build(0)
return out
print(swap_perms("abc")) # ['abc', 'acb', 'bac', 'bca', 'cba', 'cab']
Three things to know about the swap version:
- Its order is not dictionary order. Look at the end:
cbacomes beforecab. The third top-level swap exchanges positions 0 and 2, which leavesbin front ofa. If the problem wants sorted output, use the bag or sort at the end. - Either base case works.
start == len(chars) - 1is also correct for every non-empty string: when one position is left, the only swap is a letter with itself. The two versions differ only on"", which gives[""]with the first and[]with the second. - Forget the swap-back and the output goes quietly wrong. In the plain swap generator (no
triedset),abcstill returns six strings,['abc', 'acb', 'cab', 'cba', 'abc', 'acb']: two repeated and two (bac,bca) missing. A test that only checkslen(result) == 6passes. With thetriedset, the repeats are skipped and you get only the first four.
Bend the template: shorter strings and rules
Two common variations each change one line of distinct_perms.
Length-k arrangements. Change the base case to len(path) == k. With aab and k = 2 you get ['aa', 'ab', 'ba']. For distinct letters there are n! / (n − k)! of them. The output then has k × count characters, and that is the new floor.
Rules about neighbours. Say no two equal letters may touch. Check the rule before drawing, so a bad branch is never entered:
for ch in letters:
if bag[ch] == 0 or (path and path[-1] == ch):
continue
aab gives ['aba'], aabb gives ['abab', 'baba'], and aaab gives []. If you only checked the rule at the leaves, you would walk the whole tree and throw most of it away.
⚠️ A rule does not remove duplicates. Put the same neighbour check into the position template with no twin rule, and AAB returns ['ABA', 'ABA']: A₁BA₂ and A₂BA₁ both pass the check. Pruning and deduplication are separate jobs.
When your generator lies
| symptom | usual cause |
|---|---|
| too many strings, some repeated | copies treated as different letters: no bag, no twin rule, no per-level set |
| too few strings, often exactly one | an undo is missing (bag[ch] += 1, used[i] = False) |
| right count, but repeats and gaps | a plain swap generator lost its swap-back |
| a list of empty lists | path appended instead of a copy |
| strings one letter short | base case fires at len(path) == n - 1 |
Symptoms narrow the search. A brute-force reference proves the fix. itertools.permutations is slow but obviously right on short strings:
import itertools, random
def reference(s):
return sorted({"".join(p) for p in itertools.permutations(s)})
def check(generate, trials=500):
for _ in range(trials):
s = "".join(random.choice("aabc") for _ in range(random.randint(0, 6)))
got = generate(s)
assert sorted(got) == reference(s), (s, got) # also rules out repeats
print("all matched")
check(distinct_perms)
The alphabet "aabc" makes repeated letters likely. Lengths from 0 to 6 cover the empty string and single letters, and 500 trials almost surely include all-equal strings too.
Your turn: count the tile words
You have letter tiles AAB. How many different non-empty words can you spell, using each tile at most once? The answer is 8: A, B, AA, AB, BA, AAB, ABA, BAA. This is LeetCode 1079: Letter Tile Possibilities. Adapt the bag generator to count them without building any strings. What is the answer for AAABBC?
Check your answer
Every node of the tree except the root is a word: the tiles drawn on the way down. So a call returns how many words extend the tiles drawn so far, and each draw adds 1 (the word that ends with this tile) plus everything that extends it.
from collections import Counter
def num_tile_possibilities(tiles):
bag = Counter(tiles)
def count():
total = 0
for ch in bag:
if bag[ch] > 0:
bag[ch] -= 1
total += 1 + count() # this word, plus its extensions
bag[ch] += 1
return total
return count()
print(num_tile_possibilities("AAB"), num_tile_possibilities("AAABBC")) # 8 188
AAABBC gives 188. There is no base case to write: when the bag is empty, the loop adds nothing and the call returns 0. Drawing each distinct letter once per level is what keeps AB from being counted twice.
Move 2: Roll to the next arrangement
Smell: "the next lexicographically greater arrangement", "rearrange in place using only constant extra memory", "the smallest number bigger than n with the same digits", or "visit every arrangement without recursion".
Predict first: among the arrangements of the letters of dcfeba, which one comes right after dcfeba in dictionary order?
Check your answer
deabcf. The tail feba only goes down, so it is already the largest arrangement of its letters, and changing the tail alone can't make the string bigger. The c must grow. It grows by as little as possible, to e, the smallest tail letter bigger than c. Everything after it is then set as small as possible: abcf.
The slow version generates every arrangement in order, finds the current one, and returns the next. That is O(n·M) time and memory to answer a question about one neighbour. For 20 distinct letters, M = 20! ≈ 2.4 × 10¹⁸.
It wastes everything because the neighbour is close. The next string keeps the longest possible prefix and changes only a suffix. A car odometer rolls over the same way: it bumps the rightmost digit that can still go up and resets everything to its right to the smallest value. (Foundation used odometer readings for prefix sums; here only the roll-over matters.) Here the analogy stops. Odometer digits are independent, but your letters are a fixed collection. "Going up" must borrow a bigger letter from the suffix, and the "reset" must reuse exactly the letters that are left.
Pivot, successor, reverse
def reverse(a, lo, hi): # Foundation's helper: reverse a[lo..hi]
while lo < hi:
a[lo], a[hi] = a[hi], a[lo]
lo += 1
hi -= 1
def next_permutation(a):
"""Step list a to the next larger arrangement, in place.
From the largest arrangement it wraps to the smallest and returns False."""
i = len(a) - 2
while i >= 0 and a[i] >= a[i + 1]: # 1. pivot: rightmost a[i] < a[i+1]
i -= 1
if i >= 0:
j = len(a) - 1
while a[j] <= a[i]: # 2. successor: rightmost a[j] > a[i]
j -= 1
a[i], a[j] = a[j], a[i] # 3. swap them
reverse(a, i + 1, len(a) - 1) # 4. smallest order for the suffix
return i >= 0
a = list("dcfeba")
next_permutation(a)
print("".join(a)) # deabcf
Python strings are immutable, so the function works on a list of characters (Foundation, Move 5). On dcfeba:
| step | what happens | list afterwards |
|---|---|---|
| 1 | scan left from the end; c (i = 1) is the first letter smaller than its right neighbour |
dcfeba |
| 2 | rightmost suffix letter bigger than c is e (j = 3) |
dcfeba |
| 3 | swap positions 1 and 3; the suffix fcba still only goes down |
defcba |
| 4 | reverse the suffix after position 1 | deabcf |
Now start from the smallest arrangement and keep stepping. On aabb, a string with repeated letters:
| step | before | pivot i | successor j | after swap | after reverse |
|---|---|---|---|---|---|
| 1 | aabb |
1 (a) |
3 (b) |
abba |
abab |
| 2 | abab |
2 (a) |
3 (b) |
abba |
abba |
| 3 | abba |
0 (a) |
2 (b) |
bbaa |
baab |
| 4 | baab |
2 (a) |
3 (b) |
baba |
baba |
| 5 | baba |
1 (a) |
2 (b) |
bbaa |
bbaa |
| 6 | bbaa |
none | — | — | wrap, return False |
It visits six strings, which is 4! / (2! × 2!), each exactly once and in order. Duplicates need no special handling. The >= in step 1 and the <= in step 2 already treat equal letters as "not bigger".
def all_in_order(s):
a = sorted(s) # start from the smallest arrangement
out = ["".join(a)]
while next_permutation(a):
out.append("".join(a))
return out
print(all_in_order("aabb")) # ['aabb', 'abab', 'abba', 'baab', 'baba', 'bbaa']
Why it works
- The suffix after the pivot never rises. That makes it the largest arrangement of its letters, so nothing that only touches the suffix can make the string bigger. Position i must change, and keeping everything before i fixed keeps the result as close as possible.
- Position i grows by the least possible amount. The suffix never rises, so the rightmost letter bigger than a[i] is the smallest such letter.
- The swap keeps the suffix non-increasing. Letters left of j are at least a[j], which is bigger than a[i]. Letters right of j are at most a[i]. So a[i] fits into slot j without breaking the order. Reversing a non-increasing run makes it non-decreasing: the smallest arrangement of those letters.
Together, the result is the smallest string bigger than the input. Nothing lies between.
Cost. Each step is O(n) time in the worst case and uses O(1) extra space: a few indices and swaps, all in place. The character list itself takes O(n), but you need that to hold the string at all. That is what "in place, constant extra memory" in LeetCode 31: Next Permutation asks for. Enumerating everything costs O(n·M), including copying each output. There's no recursion stack and no bag.
Traps
⚠️ Strict > in step 1 treats an equal pair as a rise. On abb the pivot lands on the first b (index 1), which only equals its neighbour. Step 2 then finds no bigger letter, walks off the front of the list, and raises IndexError. Python's negative indices hide this for a few steps before it crashes. On inputs without repeats > and >= behave identically, so tests with distinct letters never catch it.
⚠️ Strict < in step 2 can swap the pivot with an equal letter: aba becomes aab, which is smaller.
Skipping the reverse gives a bigger string, but not the next one. dcfeba becomes defcba and skips everything in between.
Enumerate from the sorted string. Starting from s itself lists only the arrangements from s onward.
Know what the largest arrangement should do. LeetCode 31 wants it to wrap around to the smallest. The code above does that (with i = −1 it reverses the whole list) and returns False so a loop can stop.
Your turn: write prev_permutation(a), which steps to the next smaller arrangement. Predict what it does to baab and to aabb.
Check your answer
Mirror every comparison. The pivot is the rightmost a[i] > a[i + 1]. The successor is the rightmost a[j] < a[i]. Swap, then reverse the suffix.
def prev_permutation(a):
i = len(a) - 2
while i >= 0 and a[i] <= a[i + 1]:
i -= 1
if i >= 0:
j = len(a) - 1
while a[j] >= a[i]:
j -= 1
a[i], a[j] = a[j], a[i]
reverse(a, i + 1, len(a) - 1)
return i >= 0
baab becomes abba: step 3 of the table above, run backwards. aabb is the smallest arrangement, so there is no pivot. The function wraps it to bbaa and returns False.
Move 3: Jump to the k-th arrangement
Smell: "return the k-th permutation in order", with k anywhere up to n!.
Predict first: list the 24 arrangements of 1234 in order. Which digit starts the 9th? Which starts the 17th?
Check your answer
The first 3! = 6 arrangements start with 1, the next 6 with 2, then 6 with 3, then 6 with 4. The 9th falls in the second block, so it starts with 2. The 17th falls in the third block (13th to 18th), so it starts with 3.
The slow version starts at 123…n and calls next_permutation k − 1 times. Be honest about when that is enough. LeetCode 60: Permutation Sequence caps n at 9, so k − 1 is at most 362,879 steps, and stepping passes. It breaks when k is astronomical: with n = 20, k can reach 20! ≈ 2.4 × 10¹⁸.
What stepping repeats is walking through entire blocks one arrangement at a time, when the size of a block is one factorial.
from math import factorial
def get_permutation(n, k):
digits = [str(d) for d in range(1, n + 1)]
k -= 1 # 0-based rank
out = []
for remaining in range(n, 0, -1):
block = factorial(remaining - 1) # arrangements per choice of next digit
idx, k = divmod(k, block) # which block, and rank inside it
out.append(digits.pop(idx))
return "".join(out)
print(get_permutation(4, 9)) # 2314
get_permutation(4, 9), with 0-based rank 8:
| digits left | block | k | idx = k // block | digit taken | new k |
|---|---|---|---|---|---|
| 1234 | 6 | 8 | 1 | 2 | 2 |
| 134 | 2 | 2 | 1 | 3 | 0 |
| 14 | 1 | 0 | 0 | 1 | 0 |
| 4 | 1 | 0 | 0 | 4 | 0 |
Why it works
The invariant: before each position, k is the 0-based rank of the answer among the arrangements of the digits still unused. Those arrangements come in blocks of (remaining − 1)!, one block per possible next digit, and the blocks are in the same order as digits. divmod splits the rank into "which block" and "rank inside the block". The second part is exactly the invariant for the next position.
Cost. There are n positions, and digits.pop(idx) shifts up to n entries, so the time is O(n²). For n ≤ 9 that is nothing. (With a Fenwick tree over the unused digits, see Trees & Advanced Structures, a binary search over prefix counts finds the idx-th unused digit in O(log² n), and a Fenwick descent does it in O(log n).) Space is O(n).
Integer size. Python integers don't overflow. In Java or C++, 20! ≈ 2.43 × 10¹⁸ still fits in a signed 64-bit integer (max ≈ 9.22 × 10¹⁸), but 21! does not.
⚠️ Forgetting k -= 1 treats the 1-based k as a 0-based rank, which gives the next arrangement. get_permutation(4, 9) returns 2341 instead of 2314, and asking for the last one, k = n!, raises IndexError.
Your turn — repeated letters: find the 20th distinct arrangement of aabbc in dictionary order. The blocks are no longer equal, but the warm-up told you their sizes.
Check your answer
At each position, try the letters in order. The block for a letter ch has M(bag) × bag[ch] / remaining arrangements: that is the share rule. Skip whole blocks until the rank lands inside one.
from collections import Counter
from math import factorial
def count_arrangements(bag):
total = factorial(sum(bag.values()))
for c in bag.values():
total //= factorial(c)
return total
def kth_distinct(s, k):
bag = Counter(s)
k -= 1 # 0-based rank
out = []
for remaining in range(len(s), 0, -1):
for ch in sorted(bag):
if bag[ch] == 0:
continue
block = count_arrangements(bag) * bag[ch] // remaining
if k < block: # the answer starts with ch
out.append(ch)
bag[ch] -= 1
break
k -= block # skip ch's whole block
return "".join(out)
print(kth_distinct("aabbc", 20)) # bbaca
With rank 19: skip a's block of 12 (k = 7), take b. Then skip a's block of 6 (k = 1), take b. Take a (block 2). Skip a's block of 1 (k = 0), take c, and the last letter is a. The answer is bbaca.
Move 4: Don't generate — slide a count window
Smell: "does s2 contain a permutation of s1?", "find all anagrams of p in s", "is some rearrangement of X a substring of Y?". The question is about any arrangement, so you never need to list them.
Predict first: is some rearrangement of ab a substring of eidbaooo? At most how many substrings must you examine?
Check your answer
Yes: ba at positions 3–4. A match must be a substring of length 2, and eidbaooo has 8 − 2 + 1 = 7 of them. Whether a substring matches depends only on its letter counts, not on how ab could be ordered.
This is LeetCode 567: Permutation in String. There are two slow versions. The first generates the arrangements of s1 (up to m!, where m = len(s1)) and searches for each one, which for m = 10 means up to 3,628,800 searches. The second checks each of the n − m + 1 windows by counting its letters from scratch, at O(m) per window. The second one repeats a lot: neighbouring windows share m − 1 letters. Only one letter enters and one leaves.
Keep a count of the window and update it by those two letters:
def check_inclusion(s1, s2):
m = len(s1)
if m > len(s2):
return False
need = [0] * 26 # letter counts of s1
window = [0] * 26 # letter counts of the current window
for ch in s1:
need[ord(ch) - ord('a')] += 1
for right, ch in enumerate(s2):
window[ord(ch) - ord('a')] += 1 # ch enters
if right >= m:
window[ord(s2[right - m]) - ord('a')] -= 1 # the letter m back leaves
if window == need:
return True
return False
print(check_inclusion("ab", "eidbaooo"), check_inclusion("ab", "eidboaoo")) # True False
| right | enters | leaves | window | counts | match? |
|---|---|---|---|---|---|
| 0 | e |
— | e |
e:1 | no |
| 1 | i |
— | ei |
e:1, i:1 | no |
| 2 | d |
e |
id |
d:1, i:1 | no |
| 3 | b |
i |
db |
b:1, d:1 | no |
| 4 | a |
d |
ba |
a:1, b:1 | yes |
Why it works
Foundation's anagram fact does the heavy lifting: two strings are rearrangements of each other exactly when their count vectors are equal. The loop keeps an invariant: after processing index right, window counts the last m letters of s2 up to right (or all of them, while fewer than m have been read). The letter that leaves when s2[right] enters is s2[right - m], exactly m steps back.
The first m − 1 steps don't need a guard. A window with fewer than m letters has a smaller total than need, so it can't be equal.
Cost. Each step does O(1) updates plus a comparison of two 26-slot lists. That is O(26 · n) = O(n) time for a fixed alphabet and O(1) extra space: two arrays of 26 no matter how long the strings are.
Traps
⚠️ The leaving index. Removing s2[right - m + 1] removes a letter that is still inside the window. The code then returns False on ("ab", "eidbaooo").
Alphabet. This assumes lowercase a–z, as LeetCode 567 promises. Other characters hit Foundation's negative-index trap. For wider alphabets, use 128 slots for ASCII or a dictionary for Unicode.
Counter equality depends on the Python version. Since Python 3.10, Counter(a=1, b=0) == Counter(a=1) is True, because missing letters count as zero. Before 3.10 it was False. That is why older solutions del a letter's key when its count drops to zero.
Optional: O(1) per step for any alphabet
Comparing whole count tables costs O(alphabet) per step, which adds up when the alphabet is Unicode. Instead, keep one table diff = (count in s1) − (count in window), and track bad, the number of letters whose entry is not zero. A step changes two entries, and each change can move bad by at most one:
from collections import Counter
def check_inclusion_any_alphabet(s1, s2):
m = len(s1)
if m > len(s2):
return False
diff = Counter(s1) # count in s1 minus count in the window
bad = len(diff) # letters whose diff is not zero
def shift(ch, delta):
nonlocal bad
if diff[ch] == 0:
bad += 1 # a balanced letter is about to go off balance
diff[ch] += delta
if diff[ch] == 0:
bad -= 1 # this letter just came back into balance
for right, ch in enumerate(s2):
shift(ch, -1) # ch enters the window
if right >= m:
shift(s2[right - m], +1) # the letter m back leaves
if bad == 0:
return True
return False
The window matches exactly when bad == 0. That gives O(n + m) expected time for any alphabet, since dictionary operations are expected O(1). Space is O(number of distinct letters in s1 and the windows).
Your turn: LeetCode 438: Find All Anagrams in a String asks for every start index where an anagram of p begins in s. Both strings are non-empty and lowercase. Adapt check_inclusion, then predict the output for s = "cbaebabacd", p = "abc" and for s = "abab", p = "ab".
Check your answer
Collect the start index instead of returning True. The window logic does not change at all:
def find_anagrams(s, p):
m = len(p)
need = [0] * 26
window = [0] * 26
for ch in p:
need[ord(ch) - ord('a')] += 1
starts = []
for right, ch in enumerate(s):
window[ord(ch) - ord('a')] += 1
if right >= m:
window[ord(s[right - m]) - ord('a')] -= 1
if window == need:
starts.append(right - m + 1) # window is s[right-m+1 .. right]
return starts
print(find_anagrams("cbaebabacd", "abc"), find_anagrams("abab", "ab")) # [0, 6] [0, 1, 2]
[0, 6] (cba and bac) and [0, 1, 2]. Windows overlap, so ab, ba, ab all count. A version that jumps m letters after each match would report [0, 2].
Final round: no label on the problem
Each challenge below uses one or two moves from this lesson. Decide which before you open the answer.
Challenge 1: the next bigger number
Given a positive 32-bit integer n, return the smallest integer that uses exactly the same digits and is bigger than n. If there is none, or if it does not fit in a signed 32-bit integer (at most 2,147,483,647), return −1. This is LeetCode 556: Next Greater Element III.
Predict the results for 12, 21, 230241, 2147483476 and 2147483647.
Hint
The digits of n are a string. "Smallest bigger with the same digits" is one step of Move 2.
Check your answer
def next_greater_element(n):
a = list(str(n))
if not next_permutation(a): # digits never rise: n is already the largest
return -1
m = int("".join(a))
return m if m <= 2**31 - 1 else -1
print([next_greater_element(x) for x in (12, 21, 230241, 2147483476, 2147483647)])
# [21, -1, 230412, 2147483647, -1]
2147483476 steps to 2147483647, exactly the largest 32-bit value, so it fits. 2147483647 steps to 2147483674, which doesn't. The check is explicit because Python integers never overflow. In Java or C++, build the number in a 64-bit type before comparing. Building it digit by digit in a 32-bit int overflows: Java silently wraps, and in C++ it is undefined behaviour. The cost is O(d) time for d digits, at most 10.
Challenge 2: palindromes only
Return every distinct palindrome you can make by rearranging s (at most 16 letters), in any order. aabb gives ["abba", "baab"] and abc gives []. This is LeetCode 267: Palindrome Permutation II.
Before you code it, work out one number. A 16-letter string with eight letters twice each has how many distinct arrangements? How many of them are palindromes?
Hint
A palindrome is decided by its left half. Foundation told you when a palindrome exists at all.
Check your answer
16! / (2!)⁸ = 81,729,648,000 arrangements, but only 8! = 40,320 palindromes. Filtering the full list is hopeless. Instead:
- Count letters. If more than one count is odd, return
[](Foundation's palindrome-permutation test). - The odd letter, if any, goes in the middle.
- Put half of each count in a bag. Every distinct arrangement h of that half gives exactly one palindrome, h + middle + reversed h.
from collections import Counter
def generate_palindromes(s):
counts = Counter(s)
odd = [ch for ch, c in counts.items() if c % 2 == 1]
if len(odd) > 1:
return []
middle = odd[0] if odd else ""
half = "".join(ch * (c // 2) for ch, c in counts.items())
return [h + middle + h[::-1] for h in distinct_perms(half)]
print(generate_palindromes("aabb"), generate_palindromes("abc"), generate_palindromes("aabbc"))
# ['abba', 'baab'] [] ['abcba', 'bacab']
Distinct halves give distinct palindromes, and every palindrome comes from its own half, so there are no duplicates and nothing is missing. The cost is O(n × number of palindromes): the output size.
Challenge 3: three requests, one word
A puzzle app works with the word banana. It asks:
- How many distinct words can be made from its letters?
- Which word comes right after
bananain dictionary order? - Which word is 30th in dictionary order?
Pick a move for each, as if the word had 30 letters and listing was impossible.
Check your answer
- Count with the multinomial formula: 6! / (3! × 2! × 1!) = 60.
- Next permutation (Move 2): the pivot is the
aat index 3, the successor is thenat index 4, and after the swap and reverse you getbannaa. One O(n) step. - Block skipping with repeated letters (Move 3's
kth_distinct):annbaa. Herebananaitself is 35th.
Only a request to list all of them would need Move 1.
Cheat sheet
| When the problem… | Reach for | Key fact | Cost |
|---|---|---|---|
| asks how many arrangements | multinomial count | n! / (c₁! × … × c_k!) | O(n) arithmetic |
| wants all distinct arrangements | letter bag (or twin rule) | bag = letters not yet placed | bag O(n·M) for a fixed alphabet; twin rule O(n²·M) |
| adds a rule about neighbours | check before drawing | pruning is not deduplication | at most the unpruned cost |
| wants the next / previous arrangement | pivot, successor, reverse | suffix after the pivot never rises | O(n) time, O(1) extra |
| wants the k-th arrangement | skip whole blocks | block = arrangements of what's left | O(n²) with list.pop |
| asks if any rearrangement appears | sliding count window | equal counts ⇔ rearrangement | O(n) time, O(1) extra (26 slots) |
Before moving on, take aab and, from a blank editor, write the letter-bag generator, next_permutation, and the count window. For each one, say out loud what the key variable means at a specific moment: what bag holds before a call, why the suffix after the pivot is the largest arrangement of its letters, and which letters window counts after index right. Then name the one small edit that breaks each: a missing bag[ch] += 1, a > for >=, or right - m + 1 for right - m.
Next: Constraint Satisfaction turns "check before drawing" into a full toolkit (N-Queens, Sudoku, Word Search). String Algorithms moves from "some rearrangement appears" to finding exact patterns fast. For the general template, go back to Permutations & Combinations.