Community Problem
Beautiful Arrangement
Difficulty: Medium
Count the permutations of 1..n where every i either divides perm[i] or is divided by perm[i], using backtracking with divisibility pruning.
Beautiful Arrangement
Count the permutations of 1..n where every i either divides perm[i] or is divided by perm[i], using backtracking with divisibility pruning.
By @ethandubois
December 2, 2025
·
Updated May 18, 2026
207 views
6
4.4 (9)
I had this on a Stripe phone screen and the trick that unlocked it was placing positions in REVERSE order: positions with fewer divisors get filled first, which prunes the tree faster than left-to-right placement. Naive permutation enumeration is n!, but the divisibility constraint kills most branches; the catalog covered permutations, but it skipped this constraint-driven variant.
Beautiful Arrangement
A permutation perm of integers 1..n is called beautiful if for every index i in 1..n (1-indexed), at least one of the following holds:
perm[i]is divisible byi, ORiis divisible byperm[i].
Given an integer n, return the number of beautiful arrangements.
Examples
Example 1:
- Input:
n = 1 - Output:
1 - Explanation: The only permutation is
[1], and1 | 1holds.
Example 2:
- Input:
n = 2 - Output:
2 - Explanation:
[1, 2]and[2, 1]both satisfy the condition at every index.
Example 3:
- Input:
n = 4 - Output:
8 - Explanation: Eight of the 24 permutations of
1..4are beautiful, including[2, 1, 3, 4](check:2|2,1|2,3|3,4|4).
Example 4:
- Input:
n = 15 - Output:
24679 - Explanation: A non-trivial count that exercises the pruning logic.
Constraints
1 <= n <= 15.
Follow-up
Why fill positions back-to-front? Position i near n typically has fewer values that satisfy value % i == 0 || i % value == 0 (large i rejects most value < i). Fewer candidates means narrower branching at the top of the search tree, where pruning has the biggest payoff.
Solution
Hints
Solution Walkthrough
Approach: Backtracking, Filling Positions in Reverse (O(k) time, O(n) space)
Walk position pos from n down to 1. At each position try every unused value v in 1..n such that v % pos == 0 OR pos % v == 0. When pos == 0, increment the counter.
Key Insight
The order of position-filling matters for pruning. A position pos near n accepts only values v where pos % v == 0 or v % pos == 0, and for large pos this is a small set (most small values fail the modular check). Fewer candidates near the top of the tree means a narrower upper portion of the search and earlier dead-ends.
For example, at pos = n = 15, only v in {1, 3, 5, 15} (the divisors of 15, plus values for which 15 divides them, but no v <= n other than 15 is a multiple of 15) can be placed. So the root branches to 4 children, not 15.
Algorithm
- Allocate
used[1..n] = falseandcount = 0. - Call
backtrack(pos = n). - In
backtrack(pos):- If
pos == 0:count += 1; return. - For
vin1..n:- If
used[v], skip. - If
v % pos != 0 && pos % v != 0, skip (divisibility violation). - Mark
used[v] = true, recurse withpos - 1, unmark.
- Return
count.
Why It Works
The recursion enumerates assignments of distinct values to positions; the divisibility filter rejects illegal placements at the current position only. Since the constraint is per-index and a placed value only affects the current position, filtering inside the loop is sound. Visiting positions in reverse changes the order of the search but not its membership; the count is identical to a forward search.
Complexity
Branching factor at pos
pos=15 -> {1, 3, 5, 15} (4 candidates)
pos=14 -> {1, 2, 7, 14} minus already-used (<= 4)
pos=2 -> any even unused value or 1 (~n/2)
pos=1 -> all remaining unused values (~k)
Time O(branching product, much smaller than n!)
Space O(n) for used[] + recursion stackFor n = 15, empirical search visits ~25K leaves vs 15! = 1.3T, a 50-million-fold speedup from divisibility pruning.
Pitfalls
- Filling positions left-to-right. Position 1 accepts every value (since
v % 1 == 0always), so the top-level branching factor isn, not 4. The total work is the same in the worst case but pruning kicks in much later. - Re-allocating
used[]per recursion. Theusedarray is shared across the whole search; allocate once at the top, mark+unmark on the path. Cloning per call inflates space and time. - Forgetting the OR semantics. The condition is
v divides posORpos divides v, NOT both. Using&&gives the wrong count.
Solution Code
