Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Math & Geometry

Builds on One Choice per Slot, which you haven’t finished yet.

P2HardMental model~4 min speedrun

K-th in Dictionary Order by Block Counting (Unranking, Factorial Number System)

Permutation Sequence (LC 60)(opens leetcode.com)

The numbers 1, 2, ..., n can be put in order in n! different ways. Write all of these arrangements as strings and sort them the way a dictionary sorts words. For n = 3 the list is "123", "132", "213", "231", "312", "321", and its entries are numbered from 1.

Given n and a position k in this list, return the arrangement at position k.

Example 1
Input:n = 3, k = 3
Output:"213"
The sorted list is 123, 132, 213, 231, 312, 321, and its third entry is 213. The first two entries start with 1, so the third is the first one that starts with 2.

Count and Skip

rank k in a huge list in dictionary order ➔ at each slot, skip k // block blocks of equal size (k counted from 0) and keep k % block ➔ the k-th item, built one slot at a time
Spot it when: You must return the k-th item of a huge list sorted in dictionary order (the k-th permutation, route, string or number), or the rank of a given item in that list, and the list is far too long to build: k can reach n!, C(30, 15) or 1015.
Count and Skip
Python
1def get_permutation(n: int, k: int) -> str:
2 """Permutation Sequence: skip whole blocks of arrangements that share the digits chosen so far.
3 Time: O(N^2) | Space: O(N)
4 """
5 fact = [1] * n # fact[m] = m!
6 
7 # 1. Precompute Factorials:
8 # fact[m] holds m!, the number of arrangements of m items, needed below
9 # to size each slot's blocks without computing a factorial inside the main loop.
10 for m in range(1, n):
11 fact[m] = fact[m - 1] * m
12 
13 pool = [str(d) for d in range(1, n + 1)] # unused digits, smallest first
14 
15 # 2. Convert the Rank to 0-Based:
16 # k counts from 1, but k // block below needs the count of arrangements
17 # that come strictly before the answer, which is k - 1.
18 k -= 1
19 result = [] # digits of the answer, chosen one slot at a time
20 
21 # 3. Choose Each Slot by Skipping Whole Blocks:
22 # Every unused digit starts a block of fact[n - 1 - slot] arrangements
23 # that share the digits chosen so far; dividing k by that block size
24 # finds how many whole blocks come before the answer, and the remainder
25 # becomes the answer's rank inside the chosen block for the next slot.
26 for slot in range(n):
27 block = fact[n - 1 - slot]
28 index = k // block
29 k %= block
30 result.append(pool.pop(index))
31 return "".join(result)
Invariant: k is always the number of arrangements that start with result and come before the answer, so each slot skips exactly k // block whole blocks.

Items in dictionary order come in blocks: every arrangement that starts with the same digit sits together, and here every block at a slot holds the same number of arrangements, block = fact[n - 1 - slot]. So the answer's digit for a slot is found without listing anything: skip index = k // block whole blocks, take pool.pop(index), and continue inside the chosen block with k %= block, one slot to the right and one factorial smaller. This is correct because the blocks come in the same order as their first digits and all have the same size, so exactly index of them lie before the answer, and k % block is the answer's rank inside its own block; removing the used digit from pool keeps the next slot's choices in order. The trap is the rank's base: k counts from 1, so subtract 1 first. Without k -= 1, every k that is a multiple of block picks one block too far: n = 3, k = 3 gives "231" instead of "213".

See it run

Count and Skip, step by step

k 9 · slot - · block - · index -

k 8 · slot - · block - · index -

k 2 · slot 0 · block 6 · index 1

k 2 · slot 0 · block 6 · index 1

k 0 · slot 1 · block 2 · index 1

k 0 · slot 1 · block 2 · index 1

k 0 · slot 2 · block 1 · index 0

k 0 · slot 2 · block 1 · index 0

k 0 · slot 3 · block 1 · index 0

k 0 · slot 3 · block 1 · index 0

k 0 · slot 4 · block 1 · index 0 · answer 2314

The top row is pool, the unused digits in order. The bottom row is result, one cell per slot.

  • index the digit in pool at position k // block, the first digit of the chosen block
  • append the digit just added to result

Start: n = 4 and k = 9. fact = [1, 1, 2, 6] and pool = [1, 2, 3, 4].

k counts from 1, so k -= 1 makes k = 8, the number of arrangements before the answer. Without it, this example gives "2341".

block = fact[3] = 6. index = 8 // 6 = 1, and k becomes 8 % 6 = 2.

pool.pop(1) removes "2" and result gets it. The digits after it in pool move one place left.

block = fact[2] = 2. index = 2 // 2 = 1, and k becomes 2 % 2 = 0.

pool.pop(1) removes "3" and result gets it. The digits after it in pool move one place left.

block = fact[1] = 1. index = 0 // 1 = 0, and k becomes 0 % 1 = 0.

pool.pop(0) removes "1" and result gets it. The digits after it in pool move one place left.

block = fact[0] = 1. index = 0 // 1 = 0, and k becomes 0 % 1 = 0.

pool.pop(0) removes "4" and result gets it. The digits after it in pool move one place left.

Done: the 9-th permutation of 1 to 4 is "2314".

pool
01
12
23
34
result
1 / 11
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Leaving out k -= 1

    Mistake
    result = []
    Fix
    k -= 1
    result = []

    What happens: k counts from 1, but k // block needs the number of arrangements before the answer. That number is k - 1. Without k -= 1, every k that is a multiple of block picks one block too far.

    Example: n = 3, k = 3
    Mistake: returns '231'Fix: returns '213'
  2. 2

    Gotcha 2: Writing fact[n - slot] instead of fact[n - 1 - slot]

    Mistake
    block = fact[n - slot]
    Fix
    block = fact[n - 1 - slot]

    What happens: A block holds the arrangements of the slots after this one. At slot 0 there are n - 1 such slots. So a block there holds (n - 1)! arrangements, which is fact[n - 1]. fact has only n entries, so fact[n - 0] raises IndexError.

    Example: n = 3, k = 3
    Mistake: raises IndexErrorFix: returns '213'
  3. 3

    Gotcha 3: Reading pool[index] instead of pool.pop(index)

    Mistake
    result.append(pool[index])
    Fix
    result.append(pool.pop(index))

    What happens: pool[index] reads the digit but leaves it in pool. The next slot can choose the same digit again, so the answer repeats a digit. pool.pop(index) removes the digit, so the next slot chooses from the unused digits only.

    Example: n = 3, k = 3
    Mistake: returns '211'Fix: returns '213'
  4. 4

    Gotcha 4: Using k // block when blocks have different sizes

    What happens: k // block is correct only when every block at a slot holds the same number of arrangements. In K-th Smallest in Lexicographical Order (LC 440), the blocks at one slot differ in size. There, k // block skips the wrong number of arrangements, so it picks the wrong block.

Not this one if…
  • You get one arrangement and need the one right after it, changed in place, as in Next Permutation (LC 31): there is no rank to count down. See Next Permutation
  • You must return every combination, as in Letter Combinations of a Phone Number (LC 17), not jump to one of them by its rank. See One Choice per Slot
  • The k-th item is the k-th smallest value stored in a sorted structure, as in Kth Smallest Element in a Sorted Matrix (LC 378), not the k-th arrangement built slot by slot. See Kth Smallest
  • You count how many numbers up to n have a property, as in Number of Digit One (LC 233): there is no rank k to reach, so nothing is skipped. See Digit DP

Deep Dive

Blocks of equal size in dictionary order

Dictionary order lists the permutations of 1234 like this: 1234, 1243, 1324, and so on. All permutations that start with the same digit are next to each other. They form one block.

For n = 4 there are 24 permutations. The first 6 start with 1 and end with 1432. The next 6 start with 2 and begin with 2134. Every block at the first slot has the same size, 3! = 6, because the three other digits can be arranged in 6 ways.

In general, the block at a slot holds fact[n - 1 - slot] permutations, where fact[m] is m!. Slot 0 has blocks of fact[n - 1]. The last slot has blocks of fact[0] = 1.

So the digit for a slot can be found by arithmetic. Count how many whole blocks come before the answer. That count is k // block, and it is the position of the digit in pool.

Skipping blocks, traced for n = 4 and k = 9

k counts from 1, so the first line subtracts 1. Now k = 8 is the number of permutations before the answer. The table of fact is [1, 1, 2, 6], and pool is [1, 2, 3, 4].

Slot 0: block = fact[3] = 6. index = 8 // 6 = 1, so one whole block (the permutations that start with 1) is skipped. k becomes 8 % 6 = 2. pool.pop(1) removes 2, and pool is [1, 3, 4].

Slot 1: block = fact[2] = 2. index = 2 // 2 = 1 and k = 0. pool.pop(1) removes 3, and pool is [1, 4].

Slot 2: block = 1, index = 0, k = 0. pool.pop(0) removes 1. Slot 3 removes the last digit, 4. The result is "2314". In the full list, the 9th permutation is 2314.

The skipped blocks have the same size, and they come in the order of their first digits. So exactly index blocks lie before the answer. k % block is the rank of the answer inside its own block, and the next slot repeats the idea with a smaller block.

Why k -= 1 is needed

k // block counts the permutations before the answer, in whole blocks. The answer itself is not counted. The rank k counts the answer too, so the number before it is k - 1.

Take n = 3, k = 3. The list is 123, 132, 213, 231, 312, 321. The third item is 213. With k -= 1, k = 2. Slot 0 has block = 2, index = 1 and k = 0, and the result is "213".

Without it, k = 3, index = 3 // 2 = 1 and k = 1. Slot 1 has block = 1, index = 1, and takes 3. The result is "231", the fourth item.

The error appears whenever k is a multiple of block. For n = 4, k = 6, the answer is 1432, the last permutation of the first block. Without k -= 1, 6 // 6 = 1 skips that block, and the answer starts with 2.

Cost, limits and when blocks are not equal

Building fact costs O(N). Each slot calls pool.pop(index), which shifts the digits after index, so one slot costs O(N). The total time is O(N^2) and the space is O(N), as in the blueprint docstring.

Permutation Sequence (LC 60) limits n to 9 and k to n!, so the numbers are small. Python integers have no limit. In a language with 64-bit integers, 20! is the largest factorial that fits, so a larger n needs a cap on k or a big-number type.

The reverse question also works. The rank of "2314" is the sum of index * fact[n - 1 - slot] over all slots, plus 1. Here the sum is 1 * 6 + 1 * 2 + 0 + 0 = 8, and the rank is 9.

k // block is valid only when all blocks at a slot have the same size. In K-th Smallest in Lexicographical Order (LC 440), the numbers are 1 to n as text. For n = 13, the numbers that start with 1 are 1, 10, 11, 12, 13, which is 5 numbers. The numbers that start with 2 are only 2. Block sizes differ, so you count each prefix's block size in a loop and skip while k is larger than that size.

60-Second Pitch

"The list has n! permutations, so I do not build it. In dictionary order, all permutations with the same first digit form a block, and every block has (n - 1)! items. First I subtract 1 from k, so it counts the permutations before the answer. Then at each slot I compute index = k // block. That is how many whole blocks I skip. I take pool.pop(index) as the next digit and keep k % block for the next slot. The time is O(N^2) because of the pops, and the space is O(N). The idea works only when all blocks have the same size. When they differ, as in K-th Smallest in Lexicographical Order, I count each block's size first."

Common Questions

Why does k // block give the index of the next digit?

All permutations that start with the same digit form one block, and all blocks at a slot have the same size. Blocks come in the order of their first digit. So k // block blocks come before the answer, and the answer starts with the digit at that position in pool.

Why do you subtract 1 from k?

k is the rank, counted from 1. The division needs the number of permutations before the answer, which is k - 1. Without the subtraction, a k that is a multiple of block picks the next block. get_permutation(3, 3) would return "231" instead of "213".

What are the time and space complexity?

The time is O(N^2) and the space is O(N). The factorial table costs O(N), and each of the N slots calls pool.pop(index), which shifts up to N digits. A balanced tree over the unused digits can reduce the pops to O(log N).

How do you find the rank of a given permutation?

Run the idea backwards. At each slot, index is the position of the digit in the sorted pool. The rank is the sum of index * fact[n - 1 - slot], plus 1. For "2314" the sum is 8, so the rank is 9.

When does the idea fail, and what do you do then?

It fails when the blocks at one slot have different sizes. In K-th Smallest in Lexicographical Order (LC 440) with n = 13, the prefix 1 has 5 numbers and the prefix 2 has 1. Then you count the size of each block, and skip a block while k is larger than its size.

SWE Reasoning

Spot it

  • The problem asks for the k-th item of a list in dictionary order, or for the rank of an item (Permutation Sequence (LC 60)).
  • The list is far too long to build, because k can reach n!.
  • All items that share the chosen prefix form a block, and every block at a slot has the same size.
  • The answer is built one slot at a time, from an ordered pool of unused choices.

The invariant, step by step

  1. fact[m] holds m!, pool holds the unused digits in order, and k -= 1 turns the rank into the number of permutations before the answer.
  2. At each slot, block = fact[n - 1 - slot] is the size of one group of permutations that share the digits chosen so far.
  3. index = k // block is the number of whole blocks before the answer. The digit at pool[index] starts the answer's block.
  4. k %= block is the rank of the answer inside that block.
  5. pool.pop(index) removes the digit, so the next slot chooses from unused digits only, in order.
  6. After n slots, result holds the digits of the k-th permutation, and k is 0.

Traps

  • No k -= 1. Every k that is a multiple of block selects one block too far. For n = 3, k = 3 the result is "231" instead of "213".
  • Wrong block size. fact[n - slot] is the size of the group one slot earlier. It raises IndexError at slot 0.
  • pool[index] instead of pool.pop(index). The digit stays in pool and can be chosen again. The result for n = 3, k = 3 becomes "211".
  • Blocks of different sizes. In K-th Smallest in Lexicographical Order (LC 440), k // block with one fixed size picks the wrong block.

Practice

Canonical LeetCode:Permutation Sequence (LC 60)The k-th Lexicographical String of All Happy Strings of Length n (LC 1415)Kth Smallest Instructions (LC 1643)K-th Smallest in Lexicographical Order (LC 440)Minimum Number of Operations to Make String Sorted (LC 1830)Permutations IV (LC 3470)Smallest Palindromic Rearrangement II (LC 3518)
Theory Context•Math & Geometry

Builds on One Choice per Slot, which you haven’t finished yet.

P2HardMental model~4 min speedrun

K-th in Dictionary Order by Block Counting (Unranking, Factorial Number System)

Permutation Sequence (LC 60)(opens leetcode.com)

The numbers 1, 2, ..., n can be put in order in n! different ways. Write all of these arrangements as strings and sort them the way a dictionary sorts words. For n = 3 the list is "123", "132", "213", "231", "312", "321", and its entries are numbered from 1.

Given n and a position k in this list, return the arrangement at position k.

Example 1
Input:n = 3, k = 3
Output:"213"
The sorted list is 123, 132, 213, 231, 312, 321, and its third entry is 213. The first two entries start with 1, so the third is the first one that starts with 2.

Count and Skip

rank k in a huge list in dictionary order ➔ at each slot, skip k // block blocks of equal size (k counted from 0) and keep k % block ➔ the k-th item, built one slot at a time
Spot it when: You must return the k-th item of a huge list sorted in dictionary order (the k-th permutation, route, string or number), or the rank of a given item in that list, and the list is far too long to build: k can reach n!, C(30, 15) or 1015.
Count and Skip
Python
1def get_permutation(n: int, k: int) -> str:
2 """Permutation Sequence: skip whole blocks of arrangements that share the digits chosen so far.
3 Time: O(N^2) | Space: O(N)
4 """
5 fact = [1] * n # fact[m] = m!
6 
7 # 1. Precompute Factorials:
8 # fact[m] holds m!, the number of arrangements of m items, needed below
9 # to size each slot's blocks without computing a factorial inside the main loop.
10 for m in range(1, n):
11 fact[m] = fact[m - 1] * m
12 
13 pool = [str(d) for d in range(1, n + 1)] # unused digits, smallest first
14 
15 # 2. Convert the Rank to 0-Based:
16 # k counts from 1, but k // block below needs the count of arrangements
17 # that come strictly before the answer, which is k - 1.
18 k -= 1
19 result = [] # digits of the answer, chosen one slot at a time
20 
21 # 3. Choose Each Slot by Skipping Whole Blocks:
22 # Every unused digit starts a block of fact[n - 1 - slot] arrangements
23 # that share the digits chosen so far; dividing k by that block size
24 # finds how many whole blocks come before the answer, and the remainder
25 # becomes the answer's rank inside the chosen block for the next slot.
26 for slot in range(n):
27 block = fact[n - 1 - slot]
28 index = k // block
29 k %= block
30 result.append(pool.pop(index))
31 return "".join(result)
Invariant: k is always the number of arrangements that start with result and come before the answer, so each slot skips exactly k // block whole blocks.

Items in dictionary order come in blocks: every arrangement that starts with the same digit sits together, and here every block at a slot holds the same number of arrangements, block = fact[n - 1 - slot]. So the answer's digit for a slot is found without listing anything: skip index = k // block whole blocks, take pool.pop(index), and continue inside the chosen block with k %= block, one slot to the right and one factorial smaller. This is correct because the blocks come in the same order as their first digits and all have the same size, so exactly index of them lie before the answer, and k % block is the answer's rank inside its own block; removing the used digit from pool keeps the next slot's choices in order. The trap is the rank's base: k counts from 1, so subtract 1 first. Without k -= 1, every k that is a multiple of block picks one block too far: n = 3, k = 3 gives "231" instead of "213".

See it run

Count and Skip, step by step

k 9 · slot - · block - · index -

k 8 · slot - · block - · index -

k 2 · slot 0 · block 6 · index 1

k 2 · slot 0 · block 6 · index 1

k 0 · slot 1 · block 2 · index 1

k 0 · slot 1 · block 2 · index 1

k 0 · slot 2 · block 1 · index 0

k 0 · slot 2 · block 1 · index 0

k 0 · slot 3 · block 1 · index 0

k 0 · slot 3 · block 1 · index 0

k 0 · slot 4 · block 1 · index 0 · answer 2314

The top row is pool, the unused digits in order. The bottom row is result, one cell per slot.

  • index the digit in pool at position k // block, the first digit of the chosen block
  • append the digit just added to result

Start: n = 4 and k = 9. fact = [1, 1, 2, 6] and pool = [1, 2, 3, 4].

k counts from 1, so k -= 1 makes k = 8, the number of arrangements before the answer. Without it, this example gives "2341".

block = fact[3] = 6. index = 8 // 6 = 1, and k becomes 8 % 6 = 2.

pool.pop(1) removes "2" and result gets it. The digits after it in pool move one place left.

block = fact[2] = 2. index = 2 // 2 = 1, and k becomes 2 % 2 = 0.

pool.pop(1) removes "3" and result gets it. The digits after it in pool move one place left.

block = fact[1] = 1. index = 0 // 1 = 0, and k becomes 0 % 1 = 0.

pool.pop(0) removes "1" and result gets it. The digits after it in pool move one place left.

block = fact[0] = 1. index = 0 // 1 = 0, and k becomes 0 % 1 = 0.

pool.pop(0) removes "4" and result gets it. The digits after it in pool move one place left.

Done: the 9-th permutation of 1 to 4 is "2314".

pool
01
12
23
34
result
1 / 11
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Leaving out k -= 1

    Mistake
    result = []
    Fix
    k -= 1
    result = []

    What happens: k counts from 1, but k // block needs the number of arrangements before the answer. That number is k - 1. Without k -= 1, every k that is a multiple of block picks one block too far.

    Example: n = 3, k = 3
    Mistake: returns '231'Fix: returns '213'
  2. 2

    Gotcha 2: Writing fact[n - slot] instead of fact[n - 1 - slot]

    Mistake
    block = fact[n - slot]
    Fix
    block = fact[n - 1 - slot]

    What happens: A block holds the arrangements of the slots after this one. At slot 0 there are n - 1 such slots. So a block there holds (n - 1)! arrangements, which is fact[n - 1]. fact has only n entries, so fact[n - 0] raises IndexError.

    Example: n = 3, k = 3
    Mistake: raises IndexErrorFix: returns '213'
  3. 3

    Gotcha 3: Reading pool[index] instead of pool.pop(index)

    Mistake
    result.append(pool[index])
    Fix
    result.append(pool.pop(index))

    What happens: pool[index] reads the digit but leaves it in pool. The next slot can choose the same digit again, so the answer repeats a digit. pool.pop(index) removes the digit, so the next slot chooses from the unused digits only.

    Example: n = 3, k = 3
    Mistake: returns '211'Fix: returns '213'
  4. 4

    Gotcha 4: Using k // block when blocks have different sizes

    What happens: k // block is correct only when every block at a slot holds the same number of arrangements. In K-th Smallest in Lexicographical Order (LC 440), the blocks at one slot differ in size. There, k // block skips the wrong number of arrangements, so it picks the wrong block.

Not this one if…
  • You get one arrangement and need the one right after it, changed in place, as in Next Permutation (LC 31): there is no rank to count down. See Next Permutation
  • You must return every combination, as in Letter Combinations of a Phone Number (LC 17), not jump to one of them by its rank. See One Choice per Slot
  • The k-th item is the k-th smallest value stored in a sorted structure, as in Kth Smallest Element in a Sorted Matrix (LC 378), not the k-th arrangement built slot by slot. See Kth Smallest
  • You count how many numbers up to n have a property, as in Number of Digit One (LC 233): there is no rank k to reach, so nothing is skipped. See Digit DP

Deep Dive

Blocks of equal size in dictionary order

Dictionary order lists the permutations of 1234 like this: 1234, 1243, 1324, and so on. All permutations that start with the same digit are next to each other. They form one block.

For n = 4 there are 24 permutations. The first 6 start with 1 and end with 1432. The next 6 start with 2 and begin with 2134. Every block at the first slot has the same size, 3! = 6, because the three other digits can be arranged in 6 ways.

In general, the block at a slot holds fact[n - 1 - slot] permutations, where fact[m] is m!. Slot 0 has blocks of fact[n - 1]. The last slot has blocks of fact[0] = 1.

So the digit for a slot can be found by arithmetic. Count how many whole blocks come before the answer. That count is k // block, and it is the position of the digit in pool.

Skipping blocks, traced for n = 4 and k = 9

k counts from 1, so the first line subtracts 1. Now k = 8 is the number of permutations before the answer. The table of fact is [1, 1, 2, 6], and pool is [1, 2, 3, 4].

Slot 0: block = fact[3] = 6. index = 8 // 6 = 1, so one whole block (the permutations that start with 1) is skipped. k becomes 8 % 6 = 2. pool.pop(1) removes 2, and pool is [1, 3, 4].

Slot 1: block = fact[2] = 2. index = 2 // 2 = 1 and k = 0. pool.pop(1) removes 3, and pool is [1, 4].

Slot 2: block = 1, index = 0, k = 0. pool.pop(0) removes 1. Slot 3 removes the last digit, 4. The result is "2314". In the full list, the 9th permutation is 2314.

The skipped blocks have the same size, and they come in the order of their first digits. So exactly index blocks lie before the answer. k % block is the rank of the answer inside its own block, and the next slot repeats the idea with a smaller block.

Why k -= 1 is needed

k // block counts the permutations before the answer, in whole blocks. The answer itself is not counted. The rank k counts the answer too, so the number before it is k - 1.

Take n = 3, k = 3. The list is 123, 132, 213, 231, 312, 321. The third item is 213. With k -= 1, k = 2. Slot 0 has block = 2, index = 1 and k = 0, and the result is "213".

Without it, k = 3, index = 3 // 2 = 1 and k = 1. Slot 1 has block = 1, index = 1, and takes 3. The result is "231", the fourth item.

The error appears whenever k is a multiple of block. For n = 4, k = 6, the answer is 1432, the last permutation of the first block. Without k -= 1, 6 // 6 = 1 skips that block, and the answer starts with 2.

Cost, limits and when blocks are not equal

Building fact costs O(N). Each slot calls pool.pop(index), which shifts the digits after index, so one slot costs O(N). The total time is O(N^2) and the space is O(N), as in the blueprint docstring.

Permutation Sequence (LC 60) limits n to 9 and k to n!, so the numbers are small. Python integers have no limit. In a language with 64-bit integers, 20! is the largest factorial that fits, so a larger n needs a cap on k or a big-number type.

The reverse question also works. The rank of "2314" is the sum of index * fact[n - 1 - slot] over all slots, plus 1. Here the sum is 1 * 6 + 1 * 2 + 0 + 0 = 8, and the rank is 9.

k // block is valid only when all blocks at a slot have the same size. In K-th Smallest in Lexicographical Order (LC 440), the numbers are 1 to n as text. For n = 13, the numbers that start with 1 are 1, 10, 11, 12, 13, which is 5 numbers. The numbers that start with 2 are only 2. Block sizes differ, so you count each prefix's block size in a loop and skip while k is larger than that size.

60-Second Pitch

"The list has n! permutations, so I do not build it. In dictionary order, all permutations with the same first digit form a block, and every block has (n - 1)! items. First I subtract 1 from k, so it counts the permutations before the answer. Then at each slot I compute index = k // block. That is how many whole blocks I skip. I take pool.pop(index) as the next digit and keep k % block for the next slot. The time is O(N^2) because of the pops, and the space is O(N). The idea works only when all blocks have the same size. When they differ, as in K-th Smallest in Lexicographical Order, I count each block's size first."

Common Questions

Why does k // block give the index of the next digit?

All permutations that start with the same digit form one block, and all blocks at a slot have the same size. Blocks come in the order of their first digit. So k // block blocks come before the answer, and the answer starts with the digit at that position in pool.

Why do you subtract 1 from k?

k is the rank, counted from 1. The division needs the number of permutations before the answer, which is k - 1. Without the subtraction, a k that is a multiple of block picks the next block. get_permutation(3, 3) would return "231" instead of "213".

What are the time and space complexity?

The time is O(N^2) and the space is O(N). The factorial table costs O(N), and each of the N slots calls pool.pop(index), which shifts up to N digits. A balanced tree over the unused digits can reduce the pops to O(log N).

How do you find the rank of a given permutation?

Run the idea backwards. At each slot, index is the position of the digit in the sorted pool. The rank is the sum of index * fact[n - 1 - slot], plus 1. For "2314" the sum is 8, so the rank is 9.

When does the idea fail, and what do you do then?

It fails when the blocks at one slot have different sizes. In K-th Smallest in Lexicographical Order (LC 440) with n = 13, the prefix 1 has 5 numbers and the prefix 2 has 1. Then you count the size of each block, and skip a block while k is larger than its size.

SWE Reasoning

Spot it

  • The problem asks for the k-th item of a list in dictionary order, or for the rank of an item (Permutation Sequence (LC 60)).
  • The list is far too long to build, because k can reach n!.
  • All items that share the chosen prefix form a block, and every block at a slot has the same size.
  • The answer is built one slot at a time, from an ordered pool of unused choices.

The invariant, step by step

  1. fact[m] holds m!, pool holds the unused digits in order, and k -= 1 turns the rank into the number of permutations before the answer.
  2. At each slot, block = fact[n - 1 - slot] is the size of one group of permutations that share the digits chosen so far.
  3. index = k // block is the number of whole blocks before the answer. The digit at pool[index] starts the answer's block.
  4. k %= block is the rank of the answer inside that block.
  5. pool.pop(index) removes the digit, so the next slot chooses from unused digits only, in order.
  6. After n slots, result holds the digits of the k-th permutation, and k is 0.

Traps

  • No k -= 1. Every k that is a multiple of block selects one block too far. For n = 3, k = 3 the result is "231" instead of "213".
  • Wrong block size. fact[n - slot] is the size of the group one slot earlier. It raises IndexError at slot 0.
  • pool[index] instead of pool.pop(index). The digit stays in pool and can be chosen again. The result for n = 3, k = 3 becomes "211".
  • Blocks of different sizes. In K-th Smallest in Lexicographical Order (LC 440), k // block with one fixed size picks the wrong block.

Practice

Canonical LeetCode:Permutation Sequence (LC 60)The k-th Lexicographical String of All Happy Strings of Length n (LC 1415)Kth Smallest Instructions (LC 1643)K-th Smallest in Lexicographical Order (LC 440)Minimum Number of Operations to Make String Sorted (LC 1830)Permutations IV (LC 3470)Smallest Palindromic Rearrangement II (LC 3518)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•