questions B (backspace - burst-balloons)
This commit is contained in:
230
backend/data/questions/beautiful-arrangement.yaml
Normal file
230
backend/data/questions/beautiful-arrangement.yaml
Normal file
@@ -0,0 +1,230 @@
|
||||
title: Beautiful Arrangement
|
||||
slug: beautiful-arrangement
|
||||
difficulty: medium
|
||||
leetcode_id: 526
|
||||
leetcode_url: https://leetcode.com/problems/beautiful-arrangement/
|
||||
categories:
|
||||
- arrays
|
||||
- recursion
|
||||
patterns:
|
||||
- backtracking
|
||||
|
||||
description: |
|
||||
Suppose you have `n` integers labelled `1` through `n`. A permutation of those `n` integers `perm` (**1-indexed**) is considered a **beautiful arrangement** if for every `i` (`1 <= i <= n`), **either** of the following is true:
|
||||
|
||||
- `perm[i]` is divisible by `i`.
|
||||
- `i` is divisible by `perm[i]`.
|
||||
|
||||
Given an integer `n`, return *the **number** of the **beautiful arrangements** that you can construct*.
|
||||
|
||||
constraints: |
|
||||
- `1 <= n <= 15`
|
||||
|
||||
examples:
|
||||
- input: "n = 2"
|
||||
output: "2"
|
||||
explanation: |
|
||||
The first beautiful arrangement is [1, 2]:
|
||||
- perm[1] = 1 is divisible by i = 1
|
||||
- perm[2] = 2 is divisible by i = 2
|
||||
|
||||
The second beautiful arrangement is [2, 1]:
|
||||
- perm[1] = 2 is divisible by i = 1
|
||||
- i = 2 is divisible by perm[2] = 1
|
||||
- input: "n = 1"
|
||||
output: "1"
|
||||
explanation: "The only arrangement is [1], which satisfies perm[1] = 1 divisible by i = 1."
|
||||
|
||||
explanation:
|
||||
intuition: |
|
||||
Imagine you're filling `n` numbered boxes (positions 1 through n) with `n` numbered balls (values 1 through n). Each ball can only go in a box if there's a "divisibility relationship" between them — either the ball number divides the box number, or vice versa.
|
||||
|
||||
This is fundamentally a **constraint satisfaction problem**. You need to count all valid ways to assign balls to boxes where every assignment satisfies the divisibility rule.
|
||||
|
||||
Think of it like this: start filling boxes from position 1. For each position, try placing each unused number and check if it satisfies the divisibility condition. If it does, recursively try to fill the remaining positions. If you successfully fill all positions, you've found one beautiful arrangement. If you hit a dead end (no valid number for a position), backtrack and try a different number at the previous position.
|
||||
|
||||
The key insight is that `n <= 15` is small enough that exploring the solution space via backtracking is feasible — we won't hit time limits even though there are `n!` potential permutations, because we **prune invalid branches early**.
|
||||
|
||||
approach: |
|
||||
We solve this using **Backtracking** to explore all valid permutations:
|
||||
|
||||
**Step 1: Define the recursive function**
|
||||
|
||||
- Create a helper function `backtrack(position, used)` that tries to fill `position` using numbers from the `used` set
|
||||
- `position`: The current position we're trying to fill (1-indexed)
|
||||
- `used`: A set tracking which numbers have already been placed
|
||||
|
||||
|
||||
|
||||
**Step 2: Base case**
|
||||
|
||||
- If `position > n`, we've successfully filled all positions — increment our count and return
|
||||
- This means we found one valid beautiful arrangement
|
||||
|
||||
|
||||
|
||||
**Step 3: Try each unused number**
|
||||
|
||||
- For each number from `1` to `n`:
|
||||
- Skip if already used
|
||||
- Check the divisibility condition: `num % position == 0` or `position % num == 0`
|
||||
- If valid, mark the number as used and recurse to the next position
|
||||
- After returning, unmark the number (backtrack) to try other possibilities
|
||||
|
||||
|
||||
|
||||
**Step 4: Return the count**
|
||||
|
||||
- After exploring all branches, return the total count of valid arrangements
|
||||
|
||||
|
||||
|
||||
The backtracking naturally prunes invalid branches — if no number works for a position, that branch terminates without exploring further, making this much faster than generating all `n!` permutations.
|
||||
|
||||
common_pitfalls:
|
||||
- title: Generating All Permutations First
|
||||
description: |
|
||||
A naive approach might generate all `n!` permutations and then filter for valid ones. With `n = 15`, that's over 1.3 trillion permutations — clearly infeasible.
|
||||
|
||||
The backtracking approach is far superior because it **prunes invalid branches early**. If position 1 can only accept certain numbers, we never explore branches where position 1 has an invalid number.
|
||||
wrong_approach: "Generate all permutations, then filter"
|
||||
correct_approach: "Backtrack with early pruning on divisibility check"
|
||||
|
||||
- title: Off-by-One with 1-Indexed Positions
|
||||
description: |
|
||||
The problem uses **1-indexed** positions, meaning positions range from `1` to `n`, not `0` to `n-1`.
|
||||
|
||||
If you start your recursion at position `0` and use `position % num` or `num % position`, you'll get division by zero or incorrect results. Always start at position `1`.
|
||||
wrong_approach: "Start recursion at position 0"
|
||||
correct_approach: "Start recursion at position 1 (1-indexed)"
|
||||
|
||||
- title: Forgetting to Backtrack
|
||||
description: |
|
||||
After recursively exploring a branch with a number placed, you must **remove it from the used set** before trying the next number. Forgetting this means numbers stay "used" across different branches, leading to incorrect (lower) counts.
|
||||
|
||||
Always pair "add to used" with "remove from used" after the recursive call returns.
|
||||
wrong_approach: "Add to used set but never remove"
|
||||
correct_approach: "Remove from used set after recursive call (backtrack)"
|
||||
|
||||
key_takeaways:
|
||||
- "**Backtracking pattern**: When counting or finding all valid configurations, systematically explore choices and undo them (backtrack) to try alternatives"
|
||||
- "**Early pruning**: Check constraints as early as possible to avoid exploring invalid branches — this is what makes backtracking efficient"
|
||||
- "**Small constraints hint at exponential solutions**: When `n <= 15-20`, backtracking or bitmask DP is often the intended approach"
|
||||
- "**Foundation for harder problems**: This technique extends to N-Queens, Sudoku solvers, and other constraint satisfaction problems"
|
||||
|
||||
time_complexity: "O(k) where k is the number of valid permutations. In the worst case, this approaches O(n!) but heavy pruning makes it much faster in practice."
|
||||
space_complexity: "O(n). The recursion stack depth is at most `n`, and we use a set of size `n` to track used numbers."
|
||||
|
||||
solutions:
|
||||
- approach_name: Backtracking
|
||||
is_optimal: true
|
||||
code: |
|
||||
def count_arrangement(n: int) -> int:
|
||||
count = 0
|
||||
|
||||
def backtrack(position: int, used: set[int]) -> None:
|
||||
nonlocal count
|
||||
|
||||
# Base case: filled all positions successfully
|
||||
if position > n:
|
||||
count += 1
|
||||
return
|
||||
|
||||
# Try each number from 1 to n
|
||||
for num in range(1, n + 1):
|
||||
# Skip if already used
|
||||
if num in used:
|
||||
continue
|
||||
|
||||
# Check divisibility condition
|
||||
if num % position == 0 or position % num == 0:
|
||||
# Place this number and recurse
|
||||
used.add(num)
|
||||
backtrack(position + 1, used)
|
||||
# Backtrack: remove the number to try others
|
||||
used.remove(num)
|
||||
|
||||
backtrack(1, set())
|
||||
return count
|
||||
explanation: |
|
||||
**Time Complexity:** O(k) where k is the number of valid arrangements — significantly less than O(n!) due to pruning.
|
||||
|
||||
**Space Complexity:** O(n) — recursion depth and the used set.
|
||||
|
||||
We explore positions from 1 to n, trying each unused number that satisfies the divisibility condition. Invalid branches are pruned immediately, and we backtrack after each recursive call to explore all possibilities.
|
||||
|
||||
- approach_name: Backtracking with Visited Array
|
||||
is_optimal: true
|
||||
code: |
|
||||
def count_arrangement(n: int) -> int:
|
||||
# Track which numbers are used (index 0 unused for 1-indexing)
|
||||
visited = [False] * (n + 1)
|
||||
count = 0
|
||||
|
||||
def backtrack(position: int) -> None:
|
||||
nonlocal count
|
||||
|
||||
# Successfully filled all positions
|
||||
if position > n:
|
||||
count += 1
|
||||
return
|
||||
|
||||
for num in range(1, n + 1):
|
||||
# Skip used numbers or those failing divisibility check
|
||||
if visited[num]:
|
||||
continue
|
||||
if num % position != 0 and position % num != 0:
|
||||
continue
|
||||
|
||||
# Choose this number for current position
|
||||
visited[num] = True
|
||||
backtrack(position + 1)
|
||||
# Undo choice (backtrack)
|
||||
visited[num] = False
|
||||
|
||||
backtrack(1)
|
||||
return count
|
||||
explanation: |
|
||||
**Time Complexity:** O(k) where k is the number of valid arrangements.
|
||||
|
||||
**Space Complexity:** O(n) — visited array and recursion stack.
|
||||
|
||||
This variant uses a boolean array instead of a set for tracking used numbers. The logic is identical: try each valid number, recurse, then backtrack. Array lookups are O(1) like set operations, so performance is similar.
|
||||
|
||||
- approach_name: Bitmask Dynamic Programming
|
||||
is_optimal: true
|
||||
code: |
|
||||
def count_arrangement(n: int) -> int:
|
||||
# dp[mask] = number of ways to arrange numbers in mask
|
||||
# where mask has k bits set and we've filled positions 1..k
|
||||
dp = [0] * (1 << n)
|
||||
dp[0] = 1 # Empty arrangement: one way to arrange nothing
|
||||
|
||||
for mask in range(1 << n):
|
||||
# Count bits set = number of positions filled
|
||||
position = bin(mask).count('1') + 1
|
||||
|
||||
# If we've filled more than n positions, skip
|
||||
if position > n + 1:
|
||||
continue
|
||||
|
||||
# Try adding each number not yet in mask
|
||||
for num in range(1, n + 1):
|
||||
bit = 1 << (num - 1)
|
||||
|
||||
# Skip if number already used
|
||||
if mask & bit:
|
||||
continue
|
||||
|
||||
# Check divisibility condition
|
||||
if num % position == 0 or position % num == 0:
|
||||
dp[mask | bit] += dp[mask]
|
||||
|
||||
# Full mask (all numbers used) gives our answer
|
||||
return dp[(1 << n) - 1]
|
||||
explanation: |
|
||||
**Time Complexity:** O(n * 2^n) — iterate through all 2^n masks, trying n numbers each.
|
||||
|
||||
**Space Complexity:** O(2^n) — the DP table.
|
||||
|
||||
This approach uses bitmask DP where `dp[mask]` represents the number of ways to fill positions using exactly the numbers represented by `mask`. We iterate through all subsets and transition by adding one number at a time. With `n <= 15`, 2^15 = 32,768 states is manageable.
|
||||
Reference in New Issue
Block a user