526. Beautiful Arrangement
Problem Description
You are given a positive integer n
. Your task is to count how many permutations of the integers from 1
to n
satisfy a special "beautiful" property.
A permutation is considered beautiful if for every position i
(where 1 ≤ i ≤ n
), at least one of these conditions holds:
- The number at position
i
(denoted asperm[i]
) is divisible byi
- The position
i
is divisible by the number at that positionperm[i]
For example, if n = 2
:
- The permutation
[1, 2]
is beautiful because:- Position 1:
perm[1] = 1
, and1
is divisible by1
✓ - Position 2:
perm[2] = 2
, and2
is divisible by2
✓
- Position 1:
- The permutation
[2, 1]
is beautiful because:- Position 1:
perm[1] = 2
, and2
is divisible by1
✓ - Position 2:
perm[2] = 1
, and2
is divisible by1
✓
- Position 1:
Both permutations satisfy the beautiful arrangement criteria, so the answer would be 2
.
The solution uses a depth-first search (DFS) approach with backtracking. It first pre-computes which numbers can be placed at each position by checking the divisibility conditions. Then it recursively tries to place valid numbers at each position, marking used numbers with a visited array. When all positions are successfully filled (reaching position n + 1
), it counts that as one valid beautiful arrangement. The backtracking ensures all possible valid permutations are explored.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: While we could model this as a graph problem (with positions and numbers as nodes), the problem is fundamentally about arranging numbers in positions with specific constraints, not about traversing a graph structure.
Need to solve for kth smallest/largest?
- No: We're not looking for the kth smallest or largest element. We need to count all valid arrangements.
Involves Linked Lists?
- No: The problem deals with permutations of integers in array positions, not linked list operations.
Does the problem have small constraints?
- Yes: The constraint is
1 ≤ n ≤ 15
, which is quite small. With n=15, there are 15! possible permutations (about 1.3 trillion), but the small value of n suggests that an exponential-time algorithm is acceptable.
Brute force / Backtracking?
- Yes: Given the small constraints and the need to explore all valid permutations that satisfy specific conditions, backtracking is the ideal approach. We need to:
- Try placing each available number at each position
- Check if the placement satisfies the divisibility conditions
- Recursively continue to the next position if valid
- Backtrack when we hit invalid states
- Count all complete valid arrangements
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to systematically explore all possible permutations while pruning invalid branches early (when divisibility conditions aren't met), which is exactly what backtracking excels at.
Intuition
When we first look at this problem, we need to find all permutations where each position satisfies a divisibility relationship with its value. The key insight is that not every number can go in every position - there are constraints based on divisibility.
Let's think about this step by step. For position i
, we can only place numbers that either divide i
or are divisible by i
. For example, at position 2, we can only place numbers like 1 (since 2 is divisible by 1), 2 (since 2 is divisible by 2), 4 (since 4 is divisible by 2), 6, 8, etc.
Since we need to count all valid arrangements, we must explore every possible way to place numbers. This naturally leads us to a recursive approach where:
- We try to fill positions one by one (from position 1 to n)
- For each position, we only try numbers that satisfy the divisibility constraint
- We mark numbers as used to avoid reusing them
- We recursively fill the next position
- We backtrack by unmarking the number when we return from recursion
The clever optimization here is pre-computing which numbers are valid for each position. Before starting our recursive search, we can build a mapping that tells us: "for position i
, these are all the numbers that could potentially go here." This saves us from repeatedly checking divisibility conditions during the recursion.
For instance, if n = 4
:
- Position 1 can have: [1, 2, 3, 4] (everything divides 1)
- Position 2 can have: [1, 2, 4] (numbers that divide 2 or are divisible by 2)
- Position 3 can have: [1, 3] (numbers that divide 3 or are divisible by 3)
- Position 4 can have: [1, 2, 4] (numbers that divide 4 or are divisible by 4)
With this pre-computed information, our backtracking becomes efficient - we only explore valid candidates at each step, dramatically reducing the search space compared to trying all permutations and then checking validity.
Learn more about Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The implementation uses a depth-first search (DFS) with backtracking to systematically explore all valid arrangements. Let's break down the key components:
1. Pre-computation of Valid Matches
First, we build a dictionary match
where match[i]
contains all numbers that can be placed at position i
:
match = defaultdict(list)
for i in range(1, n + 1):
for j in range(1, n + 1):
if j % i == 0 or i % j == 0:
match[i].append(j)
This creates our constraint map. For each position i
, we check all numbers from 1 to n and store those that satisfy either j % i == 0
(number divides position) or i % j == 0
(position divides number).
2. Tracking Used Numbers
We maintain a boolean array vis
of size n + 1
to track which numbers have been used:
vis = [False] * (n + 1)
We use 1-indexed arrays to match the problem's 1-indexed positions, so vis[0]
is unused.
3. DFS with Backtracking
The core recursive function dfs(i)
tries to fill position i
:
def dfs(i):
nonlocal ans, n
if i == n + 1: # Base case: all positions filled
ans += 1
return
for j in match[i]: # Try each valid number for position i
if not vis[j]: # If number j hasn't been used
vis[j] = True # Mark as used
dfs(i + 1) # Recursively fill next position
vis[j] = False # Backtrack: unmark for other branches
4. Algorithm Flow
- Start with
dfs(1)
to fill position 1 - For each position
i
, iterate through all valid numbers inmatch[i]
- If a number hasn't been used (
vis[j] == False
), mark it as used and recursively try to fill the next position - When we successfully fill all n positions (reach position
n + 1
), increment our answer counter - After exploring a branch, backtrack by unmarking the number (
vis[j] = False
) so it can be used in other arrangements - The process continues until all valid arrangements are counted
Time Complexity: O(n!) in the worst case, as we might need to explore all permutations. However, the pre-computed constraints significantly prune the search space.
Space Complexity: O(n²) for the match dictionary plus O(n) for the recursion stack and visited array.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with n = 3
to see how the algorithm finds all beautiful arrangements.
Step 1: Pre-compute valid matches
For each position, we determine which numbers can be placed there:
- Position 1: Can place [1, 2, 3] (all numbers divide 1)
- Position 2: Can place [1, 2] (1 divides into 2, and 2 divides 2)
- Position 3: Can place [1, 3] (1 divides into 3, and 3 divides 3)
Step 2: DFS exploration starting from position 1
Initial state: vis = [False, False, False, False]
, ans = 0
dfs(1) - Fill position 1: Try number 1: vis[1] = True └─ dfs(2) - Fill position 2: Try number 2: vis[2] = True └─ dfs(3) - Fill position 3: Try number 3: vis[3] = True └─ dfs(4) → ans = 1 (Found: [1,2,3]) vis[3] = False (backtrack) vis[2] = False (backtrack) vis[1] = False (backtrack) Try number 2: vis[2] = True └─ dfs(2) - Fill position 2: Try number 1: vis[1] = True └─ dfs(3) - Fill position 3: Try number 3: vis[3] = True └─ dfs(4) → ans = 2 (Found: [2,1,3]) vis[3] = False (backtrack) vis[1] = False (backtrack) vis[2] = False (backtrack) Try number 3: vis[3] = True └─ dfs(2) - Fill position 2: Try number 1: vis[1] = True └─ dfs(3) - Fill position 3: Number 1 already used, skip Number 3 already used, skip (Dead end - no valid arrangement) vis[1] = False (backtrack) Try number 2: vis[2] = True └─ dfs(3) - Fill position 3: Try number 1: vis[1] = True └─ dfs(4) → ans = 3 (Found: [3,2,1]) vis[1] = False (backtrack) vis[2] = False (backtrack) vis[3] = False (backtrack)
Step 3: Result
The algorithm found 3 beautiful arrangements:
[1, 2, 3]
- pos1: 1|1✓, pos2: 2|2✓, pos3: 3|3✓[2, 1, 3]
- pos1: 2|1✓, pos2: 2|1✓, pos3: 3|3✓[3, 2, 1]
- pos1: 3|1✓, pos2: 2|2✓, pos3: 3|1✓
The backtracking ensures we explore all valid paths while the pre-computed matches prevent us from trying invalid placements, making the search efficient.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4 def countArrangement(self, n: int) -> int:
5 """
6 Count the number of beautiful arrangements for integers from 1 to n.
7 A beautiful arrangement means for every position i (1-indexed),
8 either num[i] % i == 0 or i % num[i] == 0.
9
10 Args:
11 n: The upper bound of integers to arrange
12
13 Returns:
14 The count of beautiful arrangements
15 """
16
17 # Build a mapping of valid numbers for each position
18 valid_numbers = defaultdict(list)
19 for position in range(1, n + 1):
20 for number in range(1, n + 1):
21 # Check if number can be placed at this position
22 if number % position == 0 or position % number == 0:
23 valid_numbers[position].append(number)
24
25 # Track which numbers have been used
26 used = [False] * (n + 1) # Index 0 unused, indices 1 to n for numbers 1 to n
27
28 # Counter for valid arrangements
29 count = 0
30
31 def backtrack(position):
32 """
33 Use backtracking to try placing numbers at each position.
34
35 Args:
36 position: Current position being filled (1-indexed)
37 """
38 nonlocal count
39
40 # Base case: all positions have been filled successfully
41 if position == n + 1:
42 count += 1
43 return
44
45 # Try each valid number for the current position
46 for number in valid_numbers[position]:
47 if not used[number]:
48 # Mark number as used
49 used[number] = True
50
51 # Recursively fill the next position
52 backtrack(position + 1)
53
54 # Backtrack: mark number as unused for other branches
55 used[number] = False
56
57 # Start filling from position 1
58 backtrack(1)
59
60 return count
61
1class Solution {
2 // Total number of elements to arrange
3 private int n;
4 // Counter for valid beautiful arrangements
5 private int count;
6 // Visited array to track which numbers have been used
7 private boolean[] visited;
8 // Map storing valid number pairs where key can be placed at position value
9 private Map<Integer, List<Integer>> validPairs;
10
11 /**
12 * Counts the number of beautiful arrangements for numbers 1 to n
13 * A beautiful arrangement means for every position i (1-indexed),
14 * either number[i] is divisible by i or i is divisible by number[i]
15 *
16 * @param n the upper bound of numbers to arrange
17 * @return the count of beautiful arrangements
18 */
19 public int countArrangement(int n) {
20 this.n = n;
21 this.count = 0;
22 this.visited = new boolean[n + 1]; // 1-indexed array
23 this.validPairs = new HashMap<>();
24
25 // Precompute all valid pairs (position, number)
26 // For each position i, find all numbers j that can be placed at position i
27 for (int position = 1; position <= n; position++) {
28 for (int number = 1; number <= n; number++) {
29 // Check if number can be placed at this position
30 if (position % number == 0 || number % position == 0) {
31 validPairs.computeIfAbsent(position, k -> new ArrayList<>()).add(number);
32 }
33 }
34 }
35
36 // Start DFS from position 1
37 dfs(1);
38 return count;
39 }
40
41 /**
42 * Depth-first search to find all valid beautiful arrangements
43 *
44 * @param position the current position being filled (1-indexed)
45 */
46 private void dfs(int position) {
47 // Base case: all positions have been filled successfully
48 if (position == n + 1) {
49 count++;
50 return;
51 }
52
53 // No valid numbers for this position (shouldn't happen with proper input)
54 if (!validPairs.containsKey(position)) {
55 return;
56 }
57
58 // Try placing each valid number at the current position
59 for (int number : validPairs.get(position)) {
60 // Only use numbers that haven't been placed yet
61 if (!visited[number]) {
62 // Mark number as used
63 visited[number] = true;
64 // Recursively fill the next position
65 dfs(position + 1);
66 // Backtrack: unmark the number for other arrangements
67 visited[number] = false;
68 }
69 }
70 }
71}
72
1class Solution {
2public:
3 int totalNumbers; // Total number of integers from 1 to n
4 int validArrangements; // Count of valid beautiful arrangements
5 vector<bool> isUsed; // Track which numbers have been used in current permutation
6 unordered_map<int, vector<int>> validMatches; // Map position -> list of valid numbers for that position
7
8 int countArrangement(int n) {
9 // Initialize member variables
10 this->totalNumbers = n;
11 this->validArrangements = 0;
12 isUsed.resize(n + 1); // Index 0 unused, indices 1 to n represent numbers 1 to n
13
14 // Precompute valid numbers for each position
15 // For position i, number j is valid if i divides j or j divides i
16 for (int position = 1; position <= n; ++position) {
17 for (int number = 1; number <= n; ++number) {
18 if (position % number == 0 || number % position == 0) {
19 validMatches[position].push_back(number);
20 }
21 }
22 }
23
24 // Start DFS from position 1
25 backtrack(1);
26
27 return validArrangements;
28 }
29
30 void backtrack(int currentPosition) {
31 // Base case: successfully placed all numbers
32 if (currentPosition == totalNumbers + 1) {
33 ++validArrangements;
34 return;
35 }
36
37 // Try placing each valid number at current position
38 for (int number : validMatches[currentPosition]) {
39 // Skip if number is already used
40 if (!isUsed[number]) {
41 // Mark number as used
42 isUsed[number] = true;
43
44 // Recursively place numbers at next position
45 backtrack(currentPosition + 1);
46
47 // Backtrack: unmark number for other permutations
48 isUsed[number] = false;
49 }
50 }
51 }
52};
53
1/**
2 * Counts the number of beautiful arrangements for integers from 1 to n
3 * A beautiful arrangement is when for every position i (1 <= i <= n):
4 * - The number at position i is divisible by i, OR
5 * - i is divisible by the number at position i
6 */
7function countArrangement(n: number): number {
8 // Track which numbers have been used in the current arrangement
9 const visited: boolean[] = new Array(n + 1).fill(false);
10
11 // Precompute valid numbers for each position to optimize DFS
12 // validNumbersForPosition[i] contains all numbers that can be placed at position i
13 const validNumbersForPosition: number[][] = Array.from({ length: n + 1 }, () => []);
14
15 // Build the valid numbers mapping for each position
16 for (let position = 1; position <= n; position++) {
17 for (let num = 1; num <= n; num++) {
18 // Check if num can be placed at position (beautiful arrangement condition)
19 if (position % num === 0 || num % position === 0) {
20 validNumbersForPosition[position].push(num);
21 }
22 }
23 }
24
25 // Counter for total number of beautiful arrangements
26 let totalArrangements = 0;
27
28 /**
29 * Depth-first search to find all beautiful arrangements
30 * @param currentPosition - The current position being filled (1-indexed)
31 * @param totalNumbers - Total number of positions to fill
32 */
33 const findArrangements = (currentPosition: number, totalNumbers: number): void => {
34 // Base case: all positions have been filled successfully
35 if (currentPosition === totalNumbers + 1) {
36 totalArrangements++;
37 return;
38 }
39
40 // Try placing each valid number at the current position
41 for (const num of validNumbersForPosition[currentPosition]) {
42 // Skip if this number has already been used
43 if (!visited[num]) {
44 // Mark number as used
45 visited[num] = true;
46
47 // Recursively fill the next position
48 findArrangements(currentPosition + 1, totalNumbers);
49
50 // Backtrack: unmark the number for other arrangements
51 visited[num] = false;
52 }
53 }
54 };
55
56 // Start DFS from position 1
57 findArrangements(1, n);
58
59 return totalArrangements;
60}
61
Time and Space Complexity
Time Complexity: O(n!)
The algorithm uses backtracking to explore all valid permutations. In the worst case, we need to explore all possible arrangements. At position 1, we have at most n
choices, at position 2 we have at most n-1
choices, and so on. This gives us an upper bound of n!
permutations to explore. While the divisibility constraints reduce the actual number of valid paths explored (pruning occurs when match[i]
has fewer options or when elements are already visited), the worst-case time complexity remains O(n!)
.
Space Complexity: O(n²)
The space complexity consists of several components:
vis
array:O(n)
space for tracking visited elementsmatch
dictionary:O(n²)
space in the worst case, as each of then
positions could potentially match with alln
numbers- Recursion call stack:
O(n)
depth in the worst case - Other variables (
ans
,i
,j
):O(1)
space
The dominant factor is the match
dictionary which stores the precomputed valid matches, resulting in an overall space complexity of O(n²)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Index Handling (0-indexed vs 1-indexed)
One of the most common mistakes is mixing up 0-indexed and 1-indexed arrays. The problem uses 1-indexed positions (positions 1 to n), but Python arrays are naturally 0-indexed.
Incorrect Implementation:
# Wrong: Using 0-indexed positions with 1-indexed problem logic
used = [False] * n # Creates array of size n
for position in range(n): # Iterates 0 to n-1
for number in range(n): # Iterates 0 to n-1
if number % position == 0: # Division by zero when position = 0!
valid_numbers[position].append(number)
Correct Implementation:
# Correct: Properly handle 1-indexed positions
used = [False] * (n + 1) # Create array of size n+1, index 0 unused
for position in range(1, n + 1): # Iterate 1 to n
for number in range(1, n + 1): # Iterate 1 to n
if number % position == 0 or position % number == 0:
valid_numbers[position].append(number)
2. Forgetting to Backtrack (Not Resetting State)
Failing to reset the used
array after exploring a branch will cause incorrect results, as numbers will remain marked as used for subsequent branches.
Incorrect Implementation:
def backtrack(position):
if position == n + 1:
count += 1
return
for number in valid_numbers[position]:
if not used[number]:
used[number] = True
backtrack(position + 1)
# Missing: used[number] = False <-- Forgot to backtrack!
Correct Implementation:
def backtrack(position):
if position == n + 1:
count += 1
return
for number in valid_numbers[position]:
if not used[number]:
used[number] = True
backtrack(position + 1)
used[number] = False # Essential: Reset state for other branches
3. Using Global Variables Instead of nonlocal
When using nested functions in Python, forgetting to declare nonlocal
for variables you want to modify can lead to UnboundLocalError or create new local variables instead of modifying the outer scope variable.
Incorrect Implementation:
def countArrangement(self, n: int) -> int:
count = 0
def backtrack(position):
if position == n + 1:
count += 1 # UnboundLocalError: local variable 'count' referenced before assignment
return
Correct Implementation:
def countArrangement(self, n: int) -> int:
count = 0
def backtrack(position):
nonlocal count # Declare that we're modifying the outer scope variable
if position == n + 1:
count += 1
return
4. Inefficient Pre-computation or No Pre-computation
Computing valid numbers on-the-fly during DFS instead of pre-computing them once leads to redundant calculations and slower performance.
Inefficient Implementation:
def backtrack(position):
if position == n + 1:
count += 1
return
# Computing valid numbers every time - inefficient!
for number in range(1, n + 1):
if not used[number]:
if number % position == 0 or position % number == 0:
used[number] = True
backtrack(position + 1)
used[number] = False
Efficient Implementation:
# Pre-compute once before DFS
valid_numbers = defaultdict(list)
for position in range(1, n + 1):
for number in range(1, n + 1):
if number % position == 0 or position % number == 0:
valid_numbers[position].append(number)
# Then use pre-computed values in DFS
def backtrack(position):
if position == n + 1:
count += 1
return
for number in valid_numbers[position]: # Use pre-computed list
if not used[number]:
used[number] = True
backtrack(position + 1)
used[number] = False
Which of the following is a min heap?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!